Compare commits

...

163 Commits
v0.5.3 ... main

Author SHA1 Message Date
trivernis 9bfb35dbce
Fix yt-dlp
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/tag/container Pipeline was successful Details
5 months ago
trivernis 048d6a5cba
Merge branch 'main' of ssh://git.trivernis.net:22321/Trivernis/2b-rs 5 months ago
trivernis b2b09a3f4f
Update animethemes-rs
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/tag/container Pipeline was successful Details
5 months ago
trivernis e3250e1bc6
Add forgejo id to container build task
ci/woodpecker/tag/container Pipeline was successful Details
5 months ago
trivernis 2d869f3ea8
Update dependency to fix build
ci/woodpecker/tag/container Pipeline failed Details
5 months ago
trivernis e6726e41c8
Update README
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/tag/container Pipeline failed Details
6 months ago
trivernis bc57733dbf
Fix build workflow
ci/woodpecker/push/build Pipeline failed Details
6 months ago
trivernis 37b22a7d51
Install clippy in build workflow
ci/woodpecker/push/build Pipeline failed Details
6 months ago
trivernis 6c9889e3e1
Increment version
ci/woodpecker/push/build Pipeline failed Details
6 months ago
trivernis 8836f311c9
Add two woodpecker workflows 6 months ago
trivernis 06c8ac2446
Fix docker build 2 years ago
trivernis 84eaf56810
Update serenity additions 2 years ago
trivernis 118832036e
Remove lavalink dependencies and update to new serenity 2 years ago
trivernis 065fc688ad
Run Rustfmt 2 years ago
trivernis 05aadf615d
Update Workflows 2 years ago
trivernis 52a956b2f7
Edit workflows 2 years ago
trivernis 7a70e30c7e
Fix Containerfile and order of migrations
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel e23a0bd325
Merge pull request #57 from Trivernis/develop
Update animethemes again
2 years ago
trivernis 4089cd2305
Update animethemes again
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 6c100ad1e5
Merge pull request #56 from Trivernis/develop
Fix issues with animethemes
2 years ago
trivernis c217e65557
Fix issues with animethemes
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 3c0cf99bef
Merge pull request #55 from Trivernis/develop
Minor fixes
2 years ago
trivernis 09fd675df0
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 898353ed57
Fix deadlocking of menus
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 34b41b78b5
Update dependencies
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 45c7f0b147
Merge pull request #53 from Trivernis/develop
Sea ORM Fun
2 years ago
trivernis 30097e624e
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 26c7df783b
Remove diesel and use sea orm instead
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 9819b58243
Remove unused variable
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 1c4450f7fc
Merge pull request #52 from Trivernis/develop
Add a twist to the fuck command
2 years ago
trivernis 9be3ef833e
Add a twist to the fuck command
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 021432ce29
Merge pull request #51 from Trivernis/develop
Develop
2 years ago
trivernis 7879062a82
Fix missing qalc executable in container
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 2ffdd1c9f9
Replace log with tracing
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 6084653203
Merge pull request #50 from Trivernis/develop
Develop
2 years ago
trivernis 0952e630ed
Change container base image to alpine
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 2716f3ad9f
Remove caching of rust builds in Containerfile (for now)
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 8a77e38ccd
Merge pull request #49 from Trivernis/develop
Develop
2 years ago
trivernis 998c51acef
Fix container build task
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 69da43cd01
Change container build workflow to podman
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 6479a44c35
Merge pull request #48 from Trivernis/develop
Update dependencies
2 years ago
trivernis 4b162e5a14
Update dependencies
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 9a023ea668
Merge pull request #46 from Trivernis/develop
Fix uptime information in status command
2 years ago
trivernis 38f2be08e4
Fix uptime information in status command
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel a1d4cb354a
Merge pull request #45 from Trivernis/develop
Update dependencies and docker build
2 years ago
trivernis 456b7e6b42
Update dependencies and docker build
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 54639162f6
Merge pull request #44 from Trivernis/develop
Update dependencies
2 years ago
trivernis 269d18c5ba
Update dependencies
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel ef08c4ffd5
Merge pull request #43 from Trivernis/develop
Update lavalink-rs to the latest git version
3 years ago
trivernis 74a22ccce9
Update lavalink-rs to the latest git version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 9cff72aef7
Merge pull request #42 from Trivernis/develop
Add missing package for openssl-sys build
3 years ago
trivernis e3b3248446
Add missing package for openssl-sys build
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 497ea686f6
Merge pull request #41 from Trivernis/develop
Update Dockerfile to fix GLIBC problems
3 years ago
trivernis 322af73473
Update Dockerfile to fix GLIBC problems
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 7bded741dd
Merge pull request #40 from Trivernis/develop
Dependency updates and stability fixes for lavalink
3 years ago
trivernis 7aeaa91e58
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 39254143a9
Update dependencies and add reset command for lavalink
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel a75deebba0
Merge pull request #39 from Trivernis/develop
Develop
3 years ago
trivernis dd0dbed8e5
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 10caa62a2b
Merge pull request #38 from Trivernis/feature/hololive-en-2
Feature/hololive en 2
3 years ago
trivernis fe55fcdb75
Add mumei command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis f087066266
Add kronii command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 53dec9740e
Merge pull request #37 from Trivernis/develop
Develop
3 years ago
trivernis 22d8162822
Add alias 'a' to gura command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 965e4fd849
Fix animethemes command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 1357d2c17c
Update rich interactions and add owners to several menus
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis e9adc2393d
Improve inspirobot embed
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 30a613b6aa
Merge pull request #36 from Trivernis/develop
Add inspirobot command
3 years ago
trivernis 0ddee0f179
Add inspirobot command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel bf2f01a46d
Merge pull request #35 from Trivernis/develop
Add party command
3 years ago
trivernis c8be3949df
Add party command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 07f610d83a
Merge pull request #34 from Trivernis/develop
Update dependencies
3 years ago
trivernis 1fd0a16f75
Update dependencies
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel 9aaf9765e1
Merge pull request #33 from Trivernis/develop
Add more hololive commands
3 years ago
trivernis e58b5af66b
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Julius Riegel b7764d477d
Merge pull request #32 from Trivernis/feature/more-hololive-commands
Feature/more hololive commands
3 years ago
trivernis a9ad68463d
Add nene command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 329ffe01c8
Add polka command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 3f7d12fe8d
Add haachama command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 63a1811cfa
Add amelia command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 2b2571c93e
Add gura command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 9e952e5d67
Add ina command and fix watame command description
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 099101a255
Update dependencies and add watame command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 6b07e16c3f
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 1028594f8d
Merge pull request #31 from Trivernis/develop
Develop
3 years ago
trivernis 40f9a90a06
Add theme command for anime themes
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 5b02a26217
Add "fuck" command
closes #30

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 28c6da036e
Merge pull request #29 from Trivernis/develop
Add qalculate to docker build
3 years ago
trivernis 30863c28e1
Add qalculate to docker build
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 5e3474a579
Merge pull request #28 from Trivernis/develop
Add xkcd command
3 years ago
trivernis 4cbb742b6f
Update xkcd-search
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 84dd3174e7
Add xkcd command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 5b14c17878
Merge pull request #27 from Trivernis/develop
Develop
3 years ago
trivernis d6fb52daf2
Change interactions registration to simpler trait based function
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 3d3568a5ea
Move bot_serenityutils to separate crate
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 46b0981b6a
Improve client initialization
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 726bee497a
Fix handling of forced vc disconnect
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis bc3cdb0cfb
Add auto deafen on voice channel join
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 88a491ebfd
Merge pull request #26 from Trivernis/develop
Fixes and Features :)
3 years ago
trivernis 47974d0d04
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 0c06cdd7db
Add miko command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 3aa831d6d1
Add fubuki command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 6f58bd2fc1
Add rushia command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis b9f9359ed7
Add korone command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 4d67dce3cd
Rename gifs to media
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 32ba3d8bf0
Fix pause being ignored on skip
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 060f5dfb64
Add delete button to equalizer
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis ef6bb00b21
Add equalize command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis df2f4786fd
Add equalizer command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 54672644a8
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 24772ac9fb
Merge pull request #25 from Trivernis/develop
Develop
3 years ago
trivernis 4f250b5375
Fix database loaded songs not having a thumbnail
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis af4a83e9ed
Add error messages to unsuccessful plays
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 18cd1e7d28
Add autoleave and fix some bugs
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis b2ba31a9e9
Reimplement all music related functionality in MusicPlayer struct
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 6876a1bb1a
Change music backend to lavalink
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis e6d89289b7
Add clear command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis de3544f916
Rename docker build to Build Docker Container
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 4d561c62cd
Merge pull request #24 from Trivernis/develop
Develop
3 years ago
trivernis 4b5d222f55
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis a4d58d33f6
Docker integration (#23)
* Add Dockerfile

Signed-off-by: trivernis <trivernis@protonmail.com>

* Fix docker build

Signed-off-by: trivernis <trivernis@protonmail.com>

* Remove build-essential package from runtime container

Signed-off-by: trivernis <trivernis@protonmail.com>

* Swap postgresql-client for libpq5 in docker build

Signed-off-by: trivernis <trivernis@protonmail.com>

* Add docker action

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis d81b815a12
Remove thumbnails from now playing message in nsfw channels
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 79be18badd
Merge pull request #22 from Trivernis/develop
Develop
3 years ago
trivernis c193298060
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 719e3e4c25
Update dependencies
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 8209c31469
Add more information to mc item command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 6f4d9df4b7
Switch to youtube-metadata for basic video information fetching
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 9cc9b720e2
Fix song database lookup being slow with spotify playlists
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 8e3ae44e32
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 5e01211b48
Merge pull request #21 from Trivernis/develop
Fix now playing message not being updated
3 years ago
trivernis 50c457ac2e
Fix now playing message not being updated
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 765e19eb03
Merge pull request #20 from Trivernis/develop
More Embed stuff
3 years ago
trivernis be4e3cf02e
Bump versions
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis fbb87fe58a
Change order of help entries to correspond to button ordering
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 43f61d51f2
Add creation of now playing message to play commmand
The message can also be deleted with the delete button.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 2fcbaef32c
Fix gpg signing in actions
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis bb153a10a5
Add help display for menus
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 70288bdc68
Merge pull request #19 from Trivernis/develop
Develop
3 years ago
trivernis fc1366668f
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis fcd1579d02
Merge pull request #18 from Trivernis/actions
Actions
3 years ago
trivernis b763371603
Add sign artifact task to release build task
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis d1e8f0f51f
Fix path in move binaries step
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 91f01c9c51
Add gpg sign test to debug action
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 3ff8371680
Change command usage descriptions to be docopt compatible
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 274804ed8c
Change DJ role check to be a serenity check
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 2ea97dca5e
Merge pull request #17 from Trivernis/develop
Develop
3 years ago
trivernis 0ce8587f2f
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis e39fc35c05
Add ephemeral message table and delete them on startup
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis c0fce89a33
Add hashtag filter to song name cleaning regex
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 017f1e8f46
Change cleaing of song names for spotify search
Now the regex is applied before all nonalphanumeric chars are deleted.
Additionally everything in square brackets will be deleted.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 6b4863fb0a
Merge branch 'main' into develop 3 years ago
trivernis 0300966e56
Fix unavailable stored videos causing songs not to be played
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 86f1cdcd46
Remove ipfs action
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis a9c559459a
Merge pull request #16 from Trivernis/actions
Change version of ipfs action
3 years ago
trivernis 3fc813bc2d
Change version of ipfs action
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis e2f9027fb9
Merge pull request #15 from Trivernis/develop
Develop
3 years ago
trivernis 26cc07abec
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 658bc7fdc5
Add feedback button and improve song results
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 77fd292763
Merge branch 'develop' of github.com:Trivernis/2b-rs into develop 3 years ago
trivernis 9931e75d8f
Add automatically adding youtube songs to store
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis d4f50d4367
Merge pull request #14 from Trivernis/actions
Add ipfs action to release build
3 years ago
trivernis 05439fbad6
Add ipfs action to release build
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis c8ab28e4e4
Add database to store spotify-youtube mappings
When playing a single youtube song the equivalent song will be
searched on spotify and stored in the database. The assumption
is that a user prefers studio versions of songs from youtube
which are harder to search for automatically.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago

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

@ -16,6 +16,12 @@ jobs:
with:
toolchain: stable
override: true
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v3
with:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
- name: Cache cargo builds
uses: actions/cache@v2
with:
@ -34,11 +40,15 @@ jobs:
run: strip target/x86_64-unknown-linux-gnu/release/tobi-rs
- name: Move binaries
run: mv target/x86_64-unknown-linux-gnu/release/tobi-rs target/tobi-rs-linux-x86_64
- 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
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: tobi-rs-linux-x86_64
path: target/tobi-rs-linux-x86_64
path: |
target/tobi-rs-linux-x86_64
target/tobi-rs-linux-x86_64.sig
- name: publish release
uses: "marvinpinto/action-automatic-releases@latest"
with:
@ -46,4 +56,5 @@ jobs:
prerelease: false
files: |
LICENSE
target/tobi-rs-linux-x86_64
target/tobi-rs-linux-x86_64
target/tobi-rs-linux-x86_64.sig

@ -0,0 +1,56 @@
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: Build and Test
name: Check
on:
workflow_dispatch:
@ -31,9 +31,15 @@ jobs:
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Run Rustfmt
uses: actions-rust-lang/rustfmt@v1.0.0
- name: Build
run: cargo build --verbose
- name: Run Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Test coreutils
run: cargo test --verbose --package bot-coreutils
@ -42,4 +48,25 @@ jobs:
run: cargo test --verbose --package bot-database
- name: Test binary
run: cargo test --verbose
run: cargo test --verbose
- name: Move binaries
run: mv target/debug/tobi-rs target/tobi-rs-linux-x86_64_debug
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v3
with:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
- 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
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: tobi-rs-linux_debug
path: |
target/tobi-rs-linux-x86_64_debug
target/tobi-rs-linux-x86_64_debug.sig

@ -0,0 +1,39 @@
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]

@ -0,0 +1,20 @@
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

4767
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,9 @@
[workspace]
members=["bot-coreutils", "bot-database", "bot-database/migration", "."]
[package]
name = "tobi-rs"
version = "0.5.3"
version = "0.11.4"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
@ -12,27 +15,41 @@ panic = 'abort'
[dependencies]
bot-database = {path="./bot-database"}
bot-coreutils = {path="./bot-coreutils"}
bot-serenityutils = {path="./bot-serenityutils"}
serenity = "0.10.5"
serenity = "0.11.5"
dotenv = "0.15.0"
tokio = { version = "1.4.0", features = ["macros", "rt-multi-thread"] }
serde_derive = "1.0.125"
serde = "1.0.125"
thiserror = "1.0.24"
minecraft-data-rs = "0.2.0"
songbird = "0.1.5"
serde_json = "1.0.64"
rand = "0.8.3"
regex = "1.4.5"
aspotify = "0.7.0"
serde_derive = "1.0.145"
serde = "1.0.145"
thiserror = "1.0.37"
minecraft-data-rs = "0.5.0"
serde_json = "1.0.86"
rand = "0.8.5"
regex = "1.6.0"
aspotify = "0.7.1"
lazy_static = "1.4.0"
futures = "0.3.13"
log = "0.4.14"
fern = "0.6.0"
chrono = "0.4.19"
colored = "2.0.0"
sysinfo = "0.16.5"
reqwest = "0.11.2"
chrono-tz = "0.5.3"
sauce-api = "0.7.1"
rustc_version_runtime = "0.2.0"
futures = "0.3.24"
chrono = "0.4.22"
sysinfo = "0.26.4"
reqwest = "0.11.12"
chrono-tz = "0.6.3"
sauce-api = "1.0.0"
rustc_version_runtime = "0.2.1"
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" }

@ -0,0 +1,45 @@
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
It's GPL 3.0
See LICENSE.md

@ -1,314 +0,0 @@
# 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
[dependencies]
tokio = {version = "1.4.0", features = ["process", "io-util"]}
log = "0.4.14"
url = "2.2.1"
mime_guess = "2.0.3"
rand = "0.8.3"
tokio = { version = "1.21.2", features = ["process", "io-util"] }
log = "0.4.17"
url = "2.3.1"
mime_guess = "2.0.4"
rand = "0.8.5"

@ -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[..]);
if stderr.len() != 0 {
log::debug!("STDERR of command {}: {}", command, stderr);
log::trace!("STDERR of command {}: {}", command, stderr);
}
log::trace!("Command output is {}", stdout);

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

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

@ -0,0 +1,13 @@
[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"] }

@ -0,0 +1,37 @@
# 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
```

@ -0,0 +1,16 @@
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),
]
}
}

@ -0,0 +1,82 @@
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(())
}
}

@ -0,0 +1,287 @@
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()
}

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

@ -1,6 +0,0 @@
-- 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();

@ -1,36 +0,0 @@
-- 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;

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

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

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

@ -1,7 +0,0 @@
-- 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)
)

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

@ -1,8 +0,0 @@
-- 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)
)

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

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

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

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

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

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

@ -0,0 +1,58 @@
use crate::entity::youtube_songs;
use crate::error::DatabaseResult;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
impl super::BotDatabase {
/// Adds a song to the database or increments the score when it
/// already exists
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_song(
&self,
spotify_id: String,
artist: String,
title: String,
album: String,
url: String,
) -> DatabaseResult<()> {
if let Some(model) = self.get_song(&spotify_id).await? {
let mut active_model: youtube_songs::ActiveModel = model.into();
active_model.score = Set(active_model.score.unwrap() + 1);
active_model.update(&self.db).await?;
} else {
let model = youtube_songs::ActiveModel {
spotify_id: Set(spotify_id),
artist: Set(artist),
title: Set(title),
album: Set(album),
url: Set(url),
..Default::default()
};
model.insert(&self.db).await?;
}
Ok(())
}
/// 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<youtube_songs::Model>> {
let song = youtube_songs::Entity::find()
.filter(youtube_songs::Column::SpotifyId.eq(spotify_id))
.one(&self.db)
.await?;
Ok(song)
}
/// Deletes a song from the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn delete_song(&self, id: i64) -> DatabaseResult<()> {
youtube_songs::Entity::delete_many()
.filter(youtube_songs::Column::Id.eq(id))
.exec(&self.db)
.await?;
Ok(())
}
}

@ -0,0 +1,24 @@
//! 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 {}

@ -0,0 +1,24 @@
//! 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 {}

@ -0,0 +1,24 @@
//! 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 {}

@ -0,0 +1,24 @@
//! 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 {}

@ -0,0 +1,10 @@
//! 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;

@ -0,0 +1,8 @@
//! 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;

@ -0,0 +1,27 @@
//! 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 {}

@ -0,0 +1,27 @@
//! 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,20 +7,8 @@ pub enum DatabaseError {
#[error("DotEnv Error: {0}")]
DotEnv(#[from] dotenv::Error),
#[error("Connection Error: {0}")]
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}")]
SeaOrm(#[from] sea_orm::error::DbErr),
#[error("{0}")]
Msg(String),

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

@ -1,58 +1,8 @@
use crate::schema::*;
use std::time::SystemTime;
#[derive(Queryable, Debug)]
pub struct GuildSetting {
pub guild_id: i64,
pub key: String,
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>,
}
use super::entity;
pub use entity::ephemeral_messages::Model as EphemeralMessage;
pub use entity::guild_playlists::Model as GuildPlaylist;
pub use entity::guild_settings::Model as GuildSetting;
pub use entity::media::Model as Media;
pub use entity::statistics::Model as Statistic;
pub use entity::youtube_songs::Model as YoutubeSong;

@ -1,42 +0,0 @@
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>,
}
}
allow_tables_to_appear_in_same_query!(
gifs,
guild_playlists,
guild_settings,
statistics,
);

@ -1 +0,0 @@
target

File diff suppressed because it is too large Load Diff

@ -1,15 +0,0 @@
[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"

@ -1,44 +0,0 @@
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)
}
}

@ -1,56 +0,0 @@
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)
}
}

@ -1,18 +0,0 @@
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),
}

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

@ -1,16 +0,0 @@
/// 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,
}
};
}

@ -1,181 +0,0 @@
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())
}

@ -1,69 +0,0 @@
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(())
}

@ -1,393 +0,0 @@
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)
}
}

@ -1,15 +0,0 @@
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;

@ -1,40 +0,0 @@
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()),
}
}
}

@ -1,40 +0,0 @@
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,40 +1,39 @@
use std::collections::{HashMap, HashSet};
use std::env;
use std::time::SystemTime;
use bot_database::get_database;
use serenity::client::Context;
use serenity::framework::standard::buckets::LimitedFor;
use serenity::framework::standard::macros::hook;
use serenity::framework::standard::{CommandResult, DispatchError};
use serenity::framework::StandardFramework;
use serenity::model::channel::Message;
use serenity::model::id::UserId;
use serenity::prelude::GatewayIntents;
use serenity::Client;
use serenity_additions::RegisterAdditions;
use songbird::SerenityInit;
use crate::commands::*;
use crate::handler::Handler;
use crate::utils::context_data::{get_database_from_context, DatabaseContainer, Store, StoreData};
use crate::handler::{get_raw_event_handler, Handler};
use crate::utils::context_data::{
get_database_from_context, DatabaseContainer, MusicPlayers, Store, StoreData,
};
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> {
let token = dotenv::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?;
let database = get_database()?;
let client = Client::builder(token)
let token = env::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?;
let database = get_database().await?;
let client = Client::builder(token, GatewayIntents::all())
.register_serenity_additions_with(get_raw_event_handler())
.event_handler(Handler)
.framework(get_framework().await)
.register_songbird()
.type_map_insert::<Store>(StoreData::create().await)
.type_map_insert::<DatabaseContainer>(database)
.type_map_insert::<MusicPlayers>(HashMap::new())
.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)
}
@ -94,13 +93,13 @@ async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Command
m.embed(|e| e.title("Error occurred").description(format!("{}", why)))
})
.await;
log::warn!("Error in {}: {:?}", cmd_name, why);
tracing::warn!("Error in {}: {:?}", cmd_name, why);
}
let database = get_database_from_context(ctx).await;
let _ = database
.add_statistic(
crate::VERSION,
cmd_name,
crate::VERSION.to_string(),
cmd_name.to_string(),
SystemTime::now(),
error_msg.is_none(),
error_msg,
@ -110,13 +109,13 @@ async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Command
#[hook]
async fn before_hook(ctx: &Context, msg: &Message, _: &str) -> bool {
log::trace!("Got command message {}", msg.content);
tracing::trace!("Got command message {}", msg.content);
let _ = msg.channel_id.broadcast_typing(ctx).await;
true
}
#[hook]
async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) {
async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError, command_name: &str) {
match error {
DispatchError::Ratelimited(info) => {
if info.is_first_try {
@ -132,13 +131,19 @@ async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) {
DispatchError::OnlyForDM => {
let _ = msg
.channel_id
.say(&ctx.http, "This command only works via DM")
.say(
&ctx.http,
format!("The command {command_name} only works via DM"),
)
.await;
}
DispatchError::OnlyForGuilds => {
let _ = msg
.channel_id
.say(&ctx.http, "This command only works on servers")
.say(
&ctx.http,
format!("The command {command_name} only works on servers"),
)
.await;
}
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 store = data.get::<Store>().expect("Failed to get store");
let enchantment_name = args.message().to_lowercase();
log::debug!("Searching for enchantment {}", enchantment_name);
tracing::debug!("Searching for enchantment {}", enchantment_name);
let enchantments_by_name = store
.minecraft_data_api
@ -29,7 +29,7 @@ pub(crate) async fn enchantment(ctx: &Context, msg: &Message, args: Args) -> Com
enchantment_name
)))?
.clone();
log::trace!("Enchantment is {:?}", enchantment);
tracing::trace!("Enchantment is {:?}", enchantment);
msg.channel_id
.send_message(ctx, |m| {

@ -1,8 +1,10 @@
use serenity::client::Context;
use serenity::framework::standard::{macros::command, Args, CommandError, CommandResult};
use serenity::framework::standard::{macros::command, Args, CommandResult};
use serenity::model::channel::Message;
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;
#[command]
@ -17,51 +19,11 @@ pub(crate) async fn item(ctx: &Context, msg: &Message, args: Args) -> CommandRes
let store = data.get::<Store>().expect("Failed to get store");
let item_name = args.message().to_lowercase();
log::debug!("Searching for item '{}'", item_name);
let items_by_name = store.minecraft_data_api.items.items_by_name()?;
let item = items_by_name
.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);
tracing::debug!("Searching for item '{}'", item_name);
let information = get_item_full_information(&item_name, &store.minecraft_data_api)?;
tracing::trace!("Item full information is {:?}", information);
create_item_message(ctx, msg.channel_id, information).await?;
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?;
Ok(())

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

@ -0,0 +1,39 @@
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(())
}

@ -0,0 +1,44 @@
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(())
}

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

@ -0,0 +1,16 @@
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,4 +1,4 @@
use crate::messages::gifs::create_gifs_menu;
use crate::messages::gifs::create_media_menu;
use crate::utils::context_data::get_database_from_context;
use serenity::client::Context;
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")]
#[bucket("general")]
#[only_in(guilds)]
async fn gifs(ctx: &Context, msg: &Message) -> CommandResult {
async fn media(ctx: &Context, msg: &Message) -> CommandResult {
let database = get_database_from_context(ctx).await;
let gifs = database.get_all_gifs().await?;
create_gifs_menu(ctx, msg.channel_id, gifs).await?;
let gifs = database.get_all_media().await?;
create_media_menu(ctx, msg.channel_id, gifs, msg.author.id).await?;
Ok(())
}

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

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

@ -0,0 +1,43 @@
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(())
}

@ -11,11 +11,11 @@ use crate::commands::common::handle_autodelete;
#[description("Shuts down the bot with the specified exit code")]
#[min_args(0)]
#[max_args(1)]
#[usage("(<code>)")]
#[usage("[<code>]")]
#[owners_only]
async fn shutdown(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let code = args.single::<i32>().unwrap_or(0);
log::info!("Shutting down with code {}...", code);
tracing::info!("Shutting down with code {}...", code);
msg.channel_id
.say(
ctx,

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

@ -9,7 +9,7 @@ use serenity::model::channel::Message;
#[description("Converts a time into a different timezone")]
#[min_args(1)]
#[max_args(3)]
#[usage("<%H:%M/now> (<from-timezone>) (<to-timezone>)")]
#[usage("(now | <%H:%M>) [<from-timezone>] [<to-timezone>]")]
#[bucket("general")]
async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let when = args.single::<String>().unwrap_or("now".to_string());
@ -17,13 +17,13 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let second_timezone = args.single::<String>().ok();
let from_timezone: Tz = if let Some(first) = &first_timezone {
forward_error!(ctx, msg.channel_id, first.parse::<Tz>())
crate::forward_error!(ctx, msg.channel_id, first.parse::<Tz>())
} else {
Tz::UTC
};
let to_timezone = if let Some(second) = &second_timezone {
forward_error!(ctx, msg.channel_id, second.parse::<Tz>())
crate::forward_error!(ctx, msg.channel_id, second.parse::<Tz>())
} else {
Tz::UTC
};
@ -33,7 +33,7 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
} else {
let now = Utc::now();
if second_timezone.is_some() {
forward_error!(
crate::forward_error!(
ctx,
msg.channel_id,
from_timezone.datetime_from_str(
@ -43,7 +43,7 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
)
} else {
let timezone: Tz = "UTC".parse().unwrap();
forward_error!(
crate::forward_error!(
ctx,
msg.channel_id,
timezone.datetime_from_str(

@ -0,0 +1,35 @@
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,12 +1,13 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::{CommandError, CommandResult};
use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_queue_for_guild, is_dj};
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -14,23 +15,21 @@ use bot_serenityutils::ephemeral_message::EphemeralMessage;
#[usage("")]
#[aliases("cq", "clear-queue", "clearqueue")]
#[bucket("general")]
#[checks(DJ)]
async fn clear_queue(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Clearing queue for guild {}", guild.id);
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
log::debug!("Clearing queue for guild {}", guild.id);
let queue = forward_error!(
ctx,
msg.channel_id,
get_queue_for_guild(ctx, &guild.id).await
);
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 queue_lock = queue.lock().await;
queue_lock.clear();
let mut player = player.lock().await;
player.queue().clear();
}
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {

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

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

@ -4,10 +4,10 @@ use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_voice_manager, is_dj};
use crate::utils::context_data::Store;
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use crate::commands::music::DJ_CHECK;
use crate::utils::context_data::MusicPlayers;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -15,48 +15,35 @@ use bot_serenityutils::ephemeral_message::EphemeralMessage;
#[usage("")]
#[aliases("stop")]
#[bucket("general")]
#[checks(DJ)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Leave request received for guild {}", guild.id);
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
let manager = get_voice_manager(ctx).await;
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);
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Leave request received for guild {}", guild.id);
if let Some(handler) = handler {
let manager = songbird::get(ctx).await.unwrap();
if let Some(handler) = manager.get(guild.id) {
let mut handler_lock = handler.lock().await;
handler_lock.remove_all_global_events();
handler_lock.leave().await?;
}
if manager.get(guild.id).is_some() {
if let Some(current) = queue_lock.current() {
current.stop()?;
let mut data = ctx.data.write().await;
let players = data.get_mut::<MusicPlayers>().unwrap();
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?;
Ok(())

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

@ -1,21 +1,20 @@
use std::mem;
use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use aspotify::Track;
use bot_database::Database;
use futures::future::BoxFuture;
use futures::FutureExt;
use regex::Regex;
use serenity::async_trait;
use serenity::client::Context;
use serenity::framework::standard::macros::group;
use serenity::http::Http;
use serenity::framework::standard::macros::{check, group};
use serenity::framework::standard::{Args, CommandOptions, Reason};
use serenity::model::channel::Message;
use serenity::model::guild::Guild;
use serenity::model::id::{ChannelId, GuildId, UserId};
use serenity::model::user::User;
use songbird::{
Call, Event, EventContext, EventHandler as VoiceEventHandler, Songbird, TrackEvent,
};
use songbird::Songbird;
use tokio::sync::Mutex;
use youtube_metadata::get_video_information;
use clear_queue::CLEAR_QUEUE_COMMAND;
use current::CURRENT_COMMAND;
@ -33,11 +32,11 @@ use save_playlist::SAVE_PLAYLIST_COMMAND;
use shuffle::SHUFFLE_COMMAND;
use skip::SKIP_COMMAND;
use crate::messages::music::now_playing::update_now_playing_msg;
use crate::providers::music::queue::{MusicQueue, Song};
use crate::providers::music::youtube_dl;
use crate::providers::music::player::MusicPlayer;
use crate::providers::music::queue::Song;
use crate::providers::music::{add_youtube_song_to_database, youtube_dl};
use crate::providers::settings::{get_setting, Setting};
use crate::utils::context_data::{DatabaseContainer, Store};
use crate::utils::context_data::{DatabaseContainer, MusicPlayers, Store};
use crate::utils::error::{BotError, BotResult};
mod clear_queue;
@ -76,116 +75,12 @@ mod skip;
)]
pub struct Music;
struct SongEndNotifier {
channel_id: ChannelId,
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)
/// Returns the voice manager from the context
pub async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> {
songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.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
.clone()
}
/// Returns the voice channel the author is in
@ -197,88 +92,15 @@ fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<Channe
.ok_or(BotError::from("You're not in a Voice Channel"))
}
/// Returns the voice manager from the context
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(
/// Returns the music player for a given guild
pub async fn get_music_player_for_guild(
ctx: &Context,
guild_id: &GuildId,
) -> BotResult<Arc<Mutex<MusicQueue>>> {
guild_id: GuildId,
) -> Option<Arc<Mutex<MusicPlayer>>> {
let data = ctx.data.read().await;
let store = data.get::<Store>().unwrap();
let players = data.get::<MusicPlayers>().unwrap();
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);
} 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
players.get(&guild_id.0).cloned()
}
/// Returns the list of songs for a given url
@ -298,23 +120,23 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe
let store = data.get::<Store>().unwrap();
let database = data.get::<DatabaseContainer>().unwrap();
log::debug!("Querying play input {}", query);
tracing::debug!("Querying play input {}", query);
if let Some(captures) = PLAYLIST_NAME_REGEX.captures(&query) {
log::debug!("Query is a saved playlist");
tracing::debug!("Query is a saved playlist");
let pl_name: &str = captures.get(1).unwrap().as_str();
log::trace!("Playlist name is {}", pl_name);
tracing::trace!("Playlist name is {}", pl_name);
let playlist_opt = database
.get_guild_playlist(guild_id.0, pl_name.to_string())
.await?;
log::trace!("Playlist is {:?}", playlist_opt);
tracing::trace!("Playlist is {:?}", playlist_opt);
if let Some(playlist) = playlist_opt {
log::debug!("Assigning url for saved playlist to query");
tracing::debug!("Assigning url for saved playlist to query");
query = playlist.url;
}
}
if YOUTUBE_URL_REGEX.is_match(&query) {
log::debug!("Query is youtube video or playlist");
tracing::debug!("Query is youtube video or playlist");
// try fetching the url as a playlist
songs = youtube_dl::get_videos_for_playlist(&query)
.await?
@ -324,37 +146,65 @@ 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 songs.len() == 0 {
log::debug!("Query is youtube video");
let mut song: Song = youtube_dl::get_video_information(&query).await?.into();
tracing::debug!("Query is youtube video");
let mut song: Song = get_video_information(&query).await?.into();
added_one_msg(&ctx, msg, &mut song).await?;
add_youtube_song_to_database(&store, &database, &mut song).await?;
songs.push(song);
} else {
log::debug!("Query is playlist with {} songs", songs.len());
tracing::debug!("Query is playlist with {} songs", songs.len());
added_multiple_msg(&ctx, msg, &mut songs).await?;
}
} else if SPOTIFY_PLAYLIST_REGEX.is_match(&query) {
// search for all songs in the playlist and search for them on youtube
log::debug!("Query is spotify playlist");
songs = store.spotify_api.get_songs_in_playlist(&query).await?;
tracing::debug!("Query is spotify playlist");
let tracks = store.spotify_api.get_songs_in_playlist(&query).await?;
let futures: Vec<BoxFuture<Song>> = tracks
.into_iter()
.map(|track| {
async {
get_youtube_song_for_track(&database, track.clone())
.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?;
} else if SPOTIFY_ALBUM_REGEX.is_match(&query) {
// fetch all songs in the album and search for them on youtube
log::debug!("Query is spotify album");
songs = store.spotify_api.get_songs_in_album(&query).await?;
tracing::debug!("Query is spotify album");
let tracks = store.spotify_api.get_songs_in_album(&query).await?;
for track in tracks {
songs.push(
get_youtube_song_for_track(&database, track.clone())
.await?
.unwrap_or(track.into()),
)
}
added_multiple_msg(&ctx, msg, &mut songs).await?;
} else if SPOTIFY_SONG_REGEX.is_match(&query) {
// fetch the song name and search it on youtube
log::debug!("Query is a spotify song");
let mut song = store.spotify_api.get_song_name(&query).await?;
tracing::debug!("Query is a spotify song");
let track = store.spotify_api.get_track_for_url(&query).await?;
let mut song = get_youtube_song_for_track(&database, track.clone())
.await?
.unwrap_or(track.into());
added_one_msg(ctx, msg, &mut song).await?;
songs.push(song);
} else {
log::debug!("Query is a youtube search");
tracing::debug!("Query is a youtube search");
let mut song: Song = youtube_dl::search_video_information(query.clone())
.await?
.ok_or(BotError::Msg(format!("Noting found for {}", query)))?
.into();
log::trace!("Search result is {:?}", song);
tracing::trace!("Search result is {:?}", song);
added_one_msg(&ctx, msg, &mut song).await?;
songs.push(song);
@ -391,6 +241,28 @@ async fn added_multiple_msg(ctx: &Context, msg: &Message, songs: &mut Vec<Song>)
Ok(())
}
#[check]
#[name = "DJ"]
pub async fn check_dj(
ctx: &Context,
msg: &Message,
_: &mut Args,
_: &CommandOptions,
) -> Result<(), Reason> {
let guild = msg
.guild(&ctx.cache)
.ok_or(Reason::Log("Not in a guild".to_string()))?;
if is_dj(ctx, guild.id, &msg.author)
.await
.map_err(|e| Reason::Log(format!("{:?}", e)))?
{
Ok(())
} else {
Err(Reason::User("Lacking DJ role".to_string()))
}
}
/// Returns if the given user is a dj in the given guild based on the
/// setting for the name of the dj role
pub async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool> {
@ -409,3 +281,27 @@ pub async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool
Ok(true)
}
}
/// 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>> {
tracing::debug!("Trying to find track in database.");
if let Some(id) = track.id {
let entry = database.get_song(&id).await?;
if let Some(song) = entry {
// check if the video is still available
tracing::trace!("Found entry is {:?}", song);
if let Ok(info) = get_video_information(&song.url).await {
return Ok(Some(info.into()));
} else {
tracing::debug!("Video '{}' is not available. Deleting entry", song.url);
database.delete_song(song.id).await?;
return Ok(None);
}
}
Ok(None)
} else {
tracing::debug!("Track has no ID");
Ok(None)
}
}

@ -1,40 +1,39 @@
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_queue_for_guild, is_dj};
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use serenity::client::Context;
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_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[description("Moves a song in the queue from one position to a new one")]
#[usage("<old-pos> <new-pos>")]
#[example("102 2")]
#[min_args(2)]
#[max_args(2)]
#[num_args(2)]
#[bucket("general")]
#[only_in(guilds)]
#[aliases("mvs", "movesong", "move-song")]
#[checks(DJ)]
async fn move_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Moving song for guild {}", guild.id);
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Moving song for guild {}", guild.id);
let pos1 = 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);
};
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
{
let queue = forward_error!(
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);
let mut player = player.lock().await;
player.queue().move_position(pos1, pos2);
}
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content(format!(

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

@ -1,68 +1,70 @@
use serenity::client::Context;
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 crate::commands::common::handle_autodelete;
use crate::commands::music::{
get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager,
join_channel, play_next_in_queue,
get_channel_for_author, get_music_player_for_guild, get_songs_for_query,
};
use crate::messages::music::now_playing::create_now_playing_msg;
use crate::providers::music::player::MusicPlayer;
use crate::providers::settings::{get_setting, Setting};
use std::sync::Arc;
#[command]
#[only_in(guilds)]
#[description("Plays a song in a voice channel")]
#[usage("(<spotify_url,youtube_url,query>)")]
#[usage("(<spotify_ur>|<youtube_url>|<query>|pl:<saved_playlist>)")]
#[min_args(1)]
#[aliases("p")]
#[bucket("music_api")]
async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message();
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Play request received for guild {}", guild.id);
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Play request received 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 channel. Joining authors channel...");
msg.guild(&ctx.cache).await.unwrap();
if player.is_none() {
tracing::debug!("Not in a channel. Joining authors channel...");
let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
handler = Some(join_channel(ctx, channel_id, guild.id).await);
let music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?;
player = Some(music_player);
}
let handler_lock = forward_error!(
ctx,
msg.channel_id,
handler.ok_or(CommandError::from("I'm not in a voice channel"))
);
let player = player.unwrap();
let songs = get_songs_for_query(&ctx, msg, query).await?;
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let play_first = {
log::debug!("Adding song to queue");
let mut queue_lock = queue.lock().await;
let (play_first, create_now_playing) = {
tracing::debug!("Adding song to queue");
let mut player_lock = player.lock().await;
for song in songs {
queue_lock.add(song);
player_lock.queue().add(song);
}
let autoshuffle = get_setting(ctx, guild.id, Setting::MusicAutoShuffle)
.await?
.unwrap_or(false);
if autoshuffle {
log::debug!("Autoshuffeling");
queue_lock.shuffle();
tracing::debug!("Autoshuffeling");
player_lock.queue().shuffle();
}
queue_lock.current().is_none()
(
player_lock.queue().current().is_none(),
player_lock.now_playing_message().is_none(),
)
};
if play_first {
log::debug!("Playing first song in queue");
while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler_lock).await {}
tracing::debug!("Playing first song in queue");
let mut player_lock = player.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?;

@ -1,62 +1,64 @@
use serenity::client::Context;
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 crate::commands::common::handle_autodelete;
use crate::commands::music::{
get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager, is_dj,
join_channel, play_next_in_queue,
get_channel_for_author, get_music_player_for_guild, get_songs_for_query, DJ_CHECK,
};
use crate::messages::music::now_playing::create_now_playing_msg;
use crate::providers::music::player::MusicPlayer;
use std::sync::Arc;
#[command]
#[only_in(guilds)]
#[description("Puts a song as the next to play in the queue")]
#[usage("<song-url>")]
#[usage("(<spotify_ur>|<youtube_url>|<query>|pl:<saved_playlist>)")]
#[min_args(1)]
#[aliases("pn", "play-next", "playnext")]
#[bucket("music_api")]
#[checks(DJ)]
async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message();
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Playing song as next song for guild {}", guild.id);
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
let manager = get_voice_manager(ctx).await;
let mut handler = manager.get(guild.id);
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Playing song as next song for guild {}", 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");
msg.guild(&ctx.cache).await.unwrap();
if player.is_none() {
tracing::debug!("Not in a channel. Joining authors channel...");
let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
handler = Some(join_channel(ctx, channel_id, guild.id).await);
let music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?;
player = Some(music_player);
}
let handler = forward_error!(
ctx,
msg.channel_id,
handler.ok_or(CommandError::from("I'm not in a voice channel"))
);
let player = player.unwrap();
let mut songs = get_songs_for_query(&ctx, msg, query).await?;
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let play_first = {
let mut queue_lock = queue.lock().await;
let (play_first, create_now_playing) = {
let mut player_lock = player.lock().await;
songs.reverse();
log::debug!("Enqueueing songs as next songs in the queue");
tracing::debug!("Enqueueing songs as next songs in the queue");
for song in songs {
queue_lock.add_next(song);
player_lock.queue().add_next(song);
}
queue_lock.current().is_none()
(
player_lock.queue().current().is_none(),
player_lock.now_playing_message().is_none(),
)
};
if play_first {
while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler).await {}
let mut player_lock = player.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?;

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

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

@ -1,39 +1,38 @@
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_queue_for_guild, is_dj};
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use serenity::client::Context;
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_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[description("Removes a song from the queue")]
#[usage("<pos>")]
#[example("102")]
#[min_args(1)]
#[max_args(1)]
#[num_args(1)]
#[bucket("general")]
#[only_in(guilds)]
#[aliases("rms", "removesong", "remove-song")]
#[checks(DJ)]
async fn remove_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Moving song for guild {}", guild.id);
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Moving song for guild {}", guild.id);
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);
};
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
{
let queue = forward_error!(
ctx,
msg.channel_id,
get_queue_for_guild(ctx, &guild.id).await
);
let mut queue_lock = queue.lock().await;
queue_lock.remove(pos);
let mut player = player.lock().await;
player.queue().remove(pos);
}
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {

@ -3,27 +3,24 @@ use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use crate::commands::music::is_dj;
use crate::commands::music::DJ_CHECK;
use crate::utils::context_data::get_database_from_context;
#[command]
#[only_in(guilds)]
#[description("Adds a playlist to the guilds saved playlists")]
#[usage("<name> <url/query>")]
#[usage("<name> (<url>|<query>")]
#[example("anime https://www.youtube.com/playlist?list=PLqaM77H_o5hykROCe3uluvZEaPo6bZj-C")]
#[min_args(2)]
#[aliases("add-playlist", "save-playlist")]
#[aliases("add-playlist", "save-playlist", "saveplaylist", "savepl")]
#[bucket("general")]
#[checks(DJ)]
async fn save_playlist(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
let name: String = args.single().unwrap();
let url: &str = args.remains().unwrap();
log::debug!(
tracing::debug!(
"Adding playlist '{}' with url '{}' to guild {}",
name,
url,

@ -1,12 +1,13 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::{CommandError, CommandResult};
use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_queue_for_guild, is_dj};
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -14,22 +15,21 @@ use bot_serenityutils::ephemeral_message::EphemeralMessage;
#[usage("")]
#[aliases("sh")]
#[bucket("general")]
#[checks(DJ)]
async fn shuffle(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
log::debug!("Shuffling queue for guild {}", guild.id);
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
let queue = forward_error!(
ctx,
msg.channel_id,
get_queue_for_guild(ctx, &guild.id).await
);
tracing::debug!("Shuffling queue for guild {}", guild.id);
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 queue_lock = queue.lock().await;
queue_lock.shuffle();
let mut player = player.lock().await;
player.queue().shuffle();
}
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {

@ -1,12 +1,13 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::{CommandError, CommandResult};
use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_queue_for_guild, is_dj};
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -14,22 +15,21 @@ use bot_serenityutils::ephemeral_message::EphemeralMessage;
#[usage("")]
#[aliases("next")]
#[bucket("general")]
#[checks(DJ)]
async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
if !is_dj(ctx, guild.id, &msg.author).await? {
msg.channel_id.say(ctx, "Requires DJ permissions").await?;
return Ok(());
}
log::debug!("Skipping song for guild {}", guild.id);
let queue = forward_error!(
ctx,
msg.channel_id,
get_queue_for_guild(ctx, &guild.id).await
);
let queue_lock = queue.lock().await;
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Skipping song for guild {}", guild.id);
if let Some(current) = queue_lock.current() {
current.stop()?;
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;
player.skip().await?;
}
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {

@ -9,7 +9,7 @@ use crate::utils::context_data::get_database_from_context;
#[command]
#[only_in(guilds)]
#[description("Get a guild setting")]
#[usage("(<setting>)")]
#[usage("[<setting>]")]
#[example("music.autoshuffle")]
#[min_args(0)]
#[max_args(1)]
@ -17,13 +17,13 @@ use crate::utils::context_data::get_database_from_context;
#[bucket("general")]
async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let database = get_database_from_context(ctx).await;
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Displaying guild setting for guild {}", guild.id);
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Displaying guild setting for guild {}", guild.id);
if let Some(key) = args.single::<String>().ok() {
log::debug!("Displaying guild setting of '{}'", key);
tracing::debug!("Displaying guild setting of '{}'", key);
let setting = database
.get_guild_setting::<String>(guild.id.0, key.clone())
.get_guild_setting::<String, _>(guild.id.0, key.clone())
.await?;
match setting {
@ -39,7 +39,7 @@ async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
}
}
} else {
log::debug!("Displaying all guild settings");
tracing::debug!("Displaying all guild settings");
let mut kv_pairs = Vec::new();
for key in ALL_SETTINGS {
@ -47,7 +47,7 @@ async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
{
match database
.get_guild_setting::<String>(guild.id.0, key.clone())
.get_guild_setting::<String, _>(guild.id.0, key.clone())
.await?
{
Some(value) => kv_pairs.push(format!("`{}` = `{}`", key, value)),

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

Loading…
Cancel
Save