Compare commits

...

213 Commits
v0.2.0 ... 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
6 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
Trivernis 25e217c5eb
Merge pull request #13 from Trivernis/develop
Develop
3 years ago
trivernis a733e6a2ac
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 49f4673213
Add aliases to qalc command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis b87b1770fc
Add autosharding
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis f1c65bb1e2
Tweak release build for smaller binaries
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 786925d8e4
Switch to stable toolchain and add more info to stats command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis b2db137663
Merge pull request #12 from Trivernis/develop
Develop
3 years ago
trivernis 8e71317d51
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 8d6ed995e0
Improve error handling with forward_error macro
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis bbf9f1ef93
Add ephemeral message and use it for most music command answers
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis eede854ad4
Fix skipping causing songs to unpause
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 3d02241642
Merge pull request #11 from Trivernis/develop
Develop
3 years ago
trivernis abd170521d
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 619d8aa30b
Add pain command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis cfbd68a7c2
Change structure of database module
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 80289e5590
Add remove_song command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis bba12ff763
Add move_song command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis a98f401fbe
Change queue command to accept query and display an embed
The queue command now displays an embed with pagination and
allows for songs to be queried by providing arguments. Each argument
is interpreted as a keyword that is searched for.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis ad69f78bd6
Merge pull request #10 from Trivernis/develop
Develop
3 years ago
trivernis 0f47c5b942
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 80dfe4af07
Add number of times used to stats command
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis a2d2d5d930
Add statistics table to database to store command statistics
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 29e670da66
Add hint about play command to now playing dialog
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis a67fecc210
Add gifs table and matsuri, add_gif and gifs command
Added a gif table to store gifs with optional category and name
as. The command add_gif (owner only) can be used to add a gif to
the database. The command gifs can be used to see all the gifs
stored in the database. The gifs are being used by the pekofy and
matsuri command.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 1bbbdc29f6
Move sauce and pekofy command to weeb group
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 2877d34ebe
Merge pull request #9 from Trivernis/develop
Develop
3 years ago
trivernis 8ed7cf90b3
Change to require dj permission for join with channel id
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 87cfba1bc8
Change menu pages to be wrapped in page type
The pages of a menu are now wrapped in an enum that contains either
a builder function or a static CreateMessage. The builder function
is used for the now playing message to always return the currently
playing song instead of mapping a static song. This solves the problem
that the old page is being rendered when the page is recreated because
of being sticky.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 411dd83240
Add controls to now playing message and fix sticky menu
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 071e5f52fd
Change menu recreation now working without cloning
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis c6b80f8abd
Add sticky option to menus
Menus can now be created as sticky menus. When new messages appear
in the channel, the sticky message will be resent to be the latest
one in the channel. It only get's recreated every ten seconds max
to avoid getting ratelimited.
To work with recreatable menus, the message handle returned by
the menu is now wrapped into an Arc<RwLock<>>.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis bf29b51092
Change music NowPlaying embed to be created as a menu
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis abc97bea45
Change MessageHandle to be a struct wrapping the IDs
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 45d2ac84b1
Add enquote util and change run_command_async
The new enquote function is used to enquote command input.
For simplicity reasons the run_command_async doesn't read from
stdout and stderr separately anymore but uses `.output().await`
instead.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 8b8fc8f814
Add rate limits to commands
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis cbdb2e3265
Change add_control on menu builder to pass the function instead of the Arc
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis bba9c1dcf3
Merge pull request #8 from Trivernis/develop
Develop
3 years ago
trivernis bcd1a96c1b
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 15a06abf22
Add random title to sauce message
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis dc8a84c51e
Implement custom embed menu in subcrate
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 6d9415ba9d
Merge pull request #7 from Trivernis/develop
Subcrate coreutils
3 years ago
trivernis 4d290a5091
Bump version and cleanup code
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis fd13f7ae64
Reimplement shuffle for VecDeque in coreutils
Introduced the new trait Shuffle that is implemented for
VecDeque and shuffles it. Previously the VecDeque
was shuffled with a function provided in the main utils module.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis d2ea577762
Move run_command_async to bot-coreutils
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 0bbbf3594f
Reimplement is_image and is_video in coreutils
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 0c2c62e157
Add subcrate bot-coreutils
Add subcrate bot-coreutils to move some utilities that don't
depend too heavily on other crates and require heavy testing
there.

Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis d54361deca
Change submodule name database to bot-databas for new naming scheme
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
Trivernis 07865cb92a
Merge pull request #6 from Trivernis/develop
Fix sauce command image extraction
3 years ago
trivernis 255e11833b
Add about command and bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago
trivernis 57292b2a5e
Fix sauce command image extraction
Signed-off-by: trivernis <trivernis@protonmail.com>
3 years ago

@ -14,13 +14,18 @@ jobs:
- name: Set up toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
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:
path: |
target
~/.cargo/
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
@ -31,13 +36,19 @@ jobs:
use-cross: false
command: build
args: --release --target x86_64-unknown-linux-gnu
- name: Strip symbols
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:
@ -45,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:
@ -20,7 +20,7 @@ jobs:
- name: Set up toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
toolchain: stable
override: true
- name: Cache build data
uses: actions/cache@v2
@ -31,9 +31,42 @@ 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
- name: Test database
run: cargo test --verbose --package bot-database
- name: Test binary
run: cargo test --verbose
- name: Move binaries
run: mv target/debug/tobi-rs target/tobi-rs-linux-x86_64_debug
- name: Run tests
run: cargo test --verbose
- 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

4742
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,33 +1,55 @@
[workspace]
members=["bot-coreutils", "bot-database", "bot-database/migration", "."]
[package]
name = "tobi-rs"
version = "0.2.0"
version = "0.11.4"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
[profile.release]
panic = 'abort'
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serenity = "0.10.5"
bot-database = {path="./bot-database"}
bot-coreutils = {path="./bot-coreutils"}
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"
database = {path="./database"}
reqwest = "0.11.2"
chrono-tz = "0.5.3"
sauce-api = "0.7.1"
serenity_utils = "0.6.1"
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

@ -0,0 +1 @@
target

@ -0,0 +1,14 @@
[package]
name = "bot-coreutils"
version = "0.1.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]
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"

@ -0,0 +1,10 @@
#[cfg(test)]
mod tests;
pub mod process;
pub mod shuffle;
pub mod string;
/// Utilities to quickly check strings that represent urls
pub mod url;
pub static VERSION: &str = env!("CARGO_PKG_VERSION");

@ -0,0 +1,19 @@
use std::io;
use tokio::process::Command;
/// Asynchronously runs a given command and returns the output
pub async fn run_command_async(command: &str, args: &[&str]) -> io::Result<String> {
log::trace!("Running command '{}' with args {:?}", command, args);
let process_output: std::process::Output = Command::new(command).args(args).output().await?;
log::trace!("Reading from stderr...");
let stderr = String::from_utf8_lossy(&process_output.stderr[..]);
let stdout = String::from_utf8_lossy(&process_output.stdout[..]);
if stderr.len() != 0 {
log::trace!("STDERR of command {}: {}", command, stderr);
}
log::trace!("Command output is {}", stdout);
Ok(stdout.to_string())
}

@ -0,0 +1,9 @@
use rand::seq::SliceRandom;
/// Chooses a random value from the given iterator
/// panics when the iterator is empty
pub fn choose_unchecked<'a, I: SliceRandom<Item = &'a T>, T>(i: I) -> &'a T {
let mut rng = rand::thread_rng();
i.choose(&mut rng).unwrap()
}

@ -0,0 +1,20 @@
use rand::Rng;
use std::collections::VecDeque;
pub trait Shuffle {
fn shuffle(&mut self);
}
impl<T> Shuffle for VecDeque<T> {
/// Fisher-Yates shuffle implementation
/// for VecDeque.
fn shuffle(&mut self) {
let mut rng = rand::thread_rng();
let mut i = self.len();
while i >= 2 {
i -= 1;
self.swap(i, rng.gen_range(0..i + 1))
}
}
}

@ -0,0 +1,5 @@
/// Enquotes a string in a safe way
pub fn enquote<S: ToString>(value: S) -> String {
let value = value.to_string();
format!("\"{}\"", value.replace("\"", "\\\""))
}

@ -0,0 +1,5 @@
#[cfg(test)]
mod url_tests;
#[cfg(test)]
mod string_tests;

@ -0,0 +1,8 @@
use crate::string::enquote;
#[test]
fn test_enquote() {
assert_eq!(enquote("hello"), r#""hello""#);
assert_eq!(enquote(r#"hello "there""#), r#""hello \"there\"""#);
assert_eq!(enquote(""), r#""""#);
}

@ -0,0 +1,44 @@
use crate::url::*;
#[test]
fn it_returns_the_domain_name() {
assert_eq!(
get_domain_for_url("https://domain.com/sub/sub"),
Some("domain.com".to_string())
);
assert_eq!(
get_domain_for_url("other-domain.com"),
Some("other-domain.com".to_string())
);
assert_eq!(get_domain_for_url("Invalid URL"), None);
assert_eq!(get_domain_for_url("file:////what/a/file.txt"), None);
assert_eq!(
get_domain_for_url("https://www.domain.com/sub",),
Some("domain.com".to_string())
);
}
#[test]
fn it_checks_for_image() {
assert!(is_image("domain.com/image.png"));
assert!(is_image("https://domain.com/image.jpeg?yo=someparam"));
assert!(!is_image("https://domain.com"));
assert!(!is_image("https://domain.com/file.pdf"));
assert!(!is_image("not an url"));
}
#[test]
fn it_checks_for_video() {
assert!(is_video("domain.com/video.mp4"));
assert!(is_video("https://domain.com/video.webm?yo=someparam"));
assert!(!is_video("https://domain.com"));
assert!(!is_video("https://domain.com/file.pdf"));
assert!(!is_video("not an url"));
}
#[test]
fn it_checks_if_its_valid() {
assert!(is_valid("https://domain.com"));
assert!(!is_valid("domain.com"));
assert!(is_valid("https://url.com/sub/sub/sub.txt"))
}

@ -0,0 +1,76 @@
use mime_guess::{mime, Mime};
pub use url::*;
static PROTOCOL_HTTP: &str = "http://";
static PROTOCOL_HTTPS: &str = "https://";
static PROTOCOL_FILE: &str = "file:////";
static PROTOCOL_DATA: &str = "data:";
static PROTOCOLS: &[&str] = &[PROTOCOL_HTTP, PROTOCOL_HTTPS, PROTOCOL_FILE, PROTOCOL_DATA];
/// Adds the protocol in front of the url if it is missing from the input
fn add_missing_protocol(url_str: &str) -> String {
for protocol in PROTOCOLS {
if url_str.starts_with(protocol) {
return url_str.to_string();
}
}
format!("{}{}", PROTOCOL_HTTPS, url_str)
}
/// Parses the given url into the url representation
/// Allows for fuzzier input than the original method. If no protocol is provided,
/// it assumes https.
#[inline]
pub fn parse_url(url_str: &str) -> Result<Url, url::ParseError> {
let url_str = add_missing_protocol(url_str);
Url::parse(&url_str)
}
/// Returns the domain for a given url string
/// Example
/// ```
/// use bot_coreutils::url::get_domain_for_url;
///
/// assert_eq!(get_domain_for_url("https://reddit.com/r/anime"), Some("reddit.com".to_string()));
/// assert_eq!(get_domain_for_url("reddit.com"), Some("reddit.com".to_string()));
/// assert_eq!(get_domain_for_url("invalid url"), None);
/// ```
pub fn get_domain_for_url(url_str: &str) -> Option<String> {
let url = parse_url(url_str).ok()?;
let domain = url.domain()?;
Some(domain.trim_start_matches("www.").to_string())
}
/// Guesses the mime for a given url string
#[inline]
fn guess_mime_for_url(url_str: &str) -> Option<Mime> {
parse_url(url_str)
.ok()
.and_then(|u| mime_guess::from_path(u.path()).first())
}
/// Returns if a given url could be an image
pub fn is_image(url_str: &str) -> bool {
if let Some(guess) = guess_mime_for_url(url_str) {
guess.type_() == mime::IMAGE
} else {
false
}
}
/// Returns if a given url could be a video
pub fn is_video(url_str: &str) -> bool {
if let Some(guess) = guess_mime_for_url(url_str) {
guess.type_() == mime::VIDEO
} else {
false
}
}
/// Returns if the given url is valid
pub fn is_valid(url_str: &str) -> bool {
Url::parse(url_str).is_ok()
}

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

@ -0,0 +1,20 @@
[package]
name = "bot-database"
version = "0.6.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dotenv = "0.15.0"
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"

@ -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;
}

@ -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(())
}
}

@ -0,0 +1,55 @@
use crate::entity::guild_playlists;
use crate::error::DatabaseResult;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
impl super::BotDatabase {
/// Returns a list of all guild playlists
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_guild_playlists(
&self,
guild_id: u64,
) -> DatabaseResult<Vec<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<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(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<()> {
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(())
}
}

@ -0,0 +1,87 @@
use sea_orm::ActiveValue::Set;
use std::any;
use std::fmt::Debug;
use std::str::FromStr;
use crate::entity::guild_settings;
use crate::error::DatabaseResult;
use sea_orm::prelude::*;
impl super::BotDatabase {
/// Returns a guild setting from the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_guild_setting<T: 'static, S: AsRef<str> + Debug>(
&self,
guild_id: u64,
key: S,
) -> DatabaseResult<Option<T>>
where
T: FromStr,
{
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?;
if let Some(setting) = setting {
if any::TypeId::of::<T>() == any::TypeId::of::<bool>() {
Ok(setting
.value
.clone()
.unwrap_or("false".to_string())
.parse::<T>()
.ok())
} else {
Ok(setting.value.clone().and_then(|v| v.parse::<T>().ok()))
}
} else {
Ok(None)
}
}
/// Upserting a guild setting
#[tracing::instrument(level = "debug", skip(self))]
pub async fn set_guild_setting<T>(
&self,
guild_id: u64,
key: String,
value: T,
) -> DatabaseResult<()>
where
T: 'static + ToString + FromStr + Debug,
{
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
#[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(())
}
}

@ -0,0 +1,25 @@
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::*;
mod ephemeral_messages;
mod guild_playlists;
mod guild_settings;
mod media;
mod statistics;
mod youtube_songs;
#[derive(Clone)]
pub struct BotDatabase {
db: DatabaseConnection,
}
impl BotDatabase {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
}

@ -0,0 +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;
#[derive(FromQueryResult)]
struct CommandCount {
count: i64,
}
impl super::BotDatabase {
/// Adds a command statistic to the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_statistic(
&self,
version: String,
command: String,
executed_at: SystemTime,
success: bool,
error_msg: Option<String>,
) -> DatabaseResult<()> {
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> {
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.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 {}

@ -0,0 +1,21 @@
use thiserror::Error;
pub type DatabaseResult<T> = Result<T, DatabaseError>;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("DotEnv Error: {0}")]
DotEnv(#[from] dotenv::Error),
#[error("{0}")]
SeaOrm(#[from] sea_orm::error::DbErr),
#[error("{0}")]
Msg(String),
}
impl From<&str> for DatabaseError {
fn from(s: &str) -> Self {
Self::Msg(s.to_string())
}
}

@ -0,0 +1,31 @@
use crate::error::DatabaseResult;
use std::env;
pub mod database;
pub mod entity;
pub mod error;
pub mod models;
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
pub use database::BotDatabase as Database;
use migration::MigratorTrait;
use sea_orm::{ConnectOptions, Database as SeaDatabase, DatabaseConnection};
#[tracing::instrument]
async fn get_connection() -> DatabaseResult<DatabaseConnection> {
let database_url = env::var("DATABASE_URL").expect("No DATABASE_URL in path");
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 async fn get_database() -> DatabaseResult<Database> {
let conn = get_connection().await?;
Ok(Database::new(conn))
}

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

467
database/Cargo.lock generated

@ -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,17 +0,0 @@
[package]
name = "database"
version = "0.1.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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"}

@ -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"

@ -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,155 +0,0 @@
use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::PoolConnection;
use diesel::prelude::*;
use diesel::{delete, insert_into};
use std::any;
use std::fmt::Debug;
use std::str::FromStr;
use tokio_diesel::*;
#[derive(Clone)]
pub struct Database {
pool: PoolConnection,
}
unsafe impl Send for Database {}
unsafe impl Sync for Database {}
impl Database {
pub fn new(pool: PoolConnection) -> Self {
Self { pool }
}
/// Returns a guild setting from the database
pub async fn get_guild_setting<T: 'static>(
&self,
guild_id: u64,
key: String,
) -> 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)
.await?;
log::trace!("Result is {:?}", entries);
if let Some(first) = entries.first() {
if any::TypeId::of::<T>() == any::TypeId::of::<bool>() {
Ok(first
.value
.clone()
.unwrap_or("false".to_string())
.parse::<T>()
.ok())
} else {
Ok(first.value.clone().and_then(|v| v.parse::<T>().ok()))
}
} else {
return Ok(None);
}
}
/// Upserting a guild setting
pub async fn set_guild_setting<T>(
&self,
guild_id: u64,
key: String,
value: T,
) -> DatabaseResult<()>
where
T: ToString + 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?;
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)
.await?;
Ok(())
}
/// 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)
.await?;
Ok(playlists)
}
/// Returns a guild playlist by name
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)
.await?;
Ok(playlists.into_iter().next())
}
/// Adds a new playlist to the database overwriting the old one
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?;
Ok(())
}
}

@ -1,24 +0,0 @@
use thiserror::Error;
pub type DatabaseResult<T> = Result<T, DatabaseError>;
#[derive(Error, Debug)]
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),
}

@ -1,41 +0,0 @@
#[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 error;
pub mod models;
pub mod schema;
pub use database::Database;
type PoolConnection = Pool<ConnectionManager<PgConnection>>;
embed_migrations!("../database/migrations");
fn get_connection() -> DatabaseResult<PoolConnection> {
dotenv::dotenv()?;
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)
}
pub fn get_database() -> DatabaseResult<Database> {
let conn = get_connection()?;
Ok(Database::new(conn))
}

@ -1,31 +0,0 @@
use crate::schema::*;
#[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,
}

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

@ -1,5 +1,5 @@
[toolchain]
channel = "nightly"
channel = "stable"
targets = [
"x86_64-unknown-linux-gnu",
]

@ -1,41 +1,44 @@
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::handler::{get_raw_event_handler, Handler};
use crate::utils::context_data::{
DatabaseContainer, EventDrivenMessageContainer, Store, StoreData,
get_database_from_context, DatabaseContainer, MusicPlayers, Store, StoreData,
};
use crate::utils::error::{BotError, BotResult};
use database::get_database;
use serenity::model::id::UserId;
use std::collections::{HashMap, HashSet};
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())
.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>(HashMap::new());
}
Ok(client)
}
pub fn get_framework() -> StandardFramework {
pub async fn get_framework() -> StandardFramework {
let mut owners = HashSet::new();
if let Some(owner) = dotenv::var("BOT_OWNER").ok().and_then(|o| o.parse().ok()) {
owners.insert(UserId(owner));
@ -55,35 +58,64 @@ pub fn get_framework() -> StandardFramework {
.group(&MISC_GROUP)
.group(&MUSIC_GROUP)
.group(&SETTINGS_GROUP)
.group(&WEEB_GROUP)
.after(after_hook)
.before(before_hook)
.on_dispatch_error(dispatch_error)
.help(&HELP)
.bucket("music_api", |b| {
b.delay(1)
.time_span(60)
.limit(30)
.limit_for(LimitedFor::User)
})
.await
.bucket("sauce_api", |b| {
b.delay(1)
.time_span(60)
.limit(10)
.limit_for(LimitedFor::User)
})
.await
.bucket("general", |b| b.time_span(10).limit(5))
.await
}
#[hook]
async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: CommandResult) {
// Print out an error if it happened
let mut error_msg = None;
if let Err(why) = error {
error_msg = Some(why.to_string());
let _ = msg
.channel_id
.send_message(ctx, |m| {
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.to_string(),
cmd_name.to_string(),
SystemTime::now(),
error_msg.is_none(),
error_msg,
)
.await;
}
#[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 {
@ -99,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 } => {

@ -1,8 +1,9 @@
use crate::providers::settings::{get_setting, Setting};
use crate::utils::error::BotResult;
use serenity::model::channel::Message;
use serenity::prelude::*;
use crate::providers::settings::{get_setting, Setting};
use crate::utils::error::BotResult;
/// Deletes a message automatically if configured that way
pub async fn handle_autodelete(ctx: &Context, msg: &Message) -> BotResult<()> {
if let Some(guild_id) = msg.guild_id {

@ -11,11 +11,12 @@ use crate::utils::context_data::Store;
#[example("unbreaking")]
#[min_args(1)]
#[aliases("ench")]
#[bucket("general")]
pub(crate) async fn enchantment(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
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
@ -28,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]
@ -11,56 +13,17 @@ use crate::utils::context_data::Store;
#[example("bread")]
#[min_args(1)]
#[aliases("i")]
#[bucket("general")]
pub(crate) async fn item(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await;
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(())

@ -0,0 +1,25 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
#[command]
#[description("Displays information about the bot")]
#[bucket("general")]
async fn about(ctx: &Context, msg: &Message) -> CommandResult {
msg.channel_id
.send_message(ctx, |m| {
m.embed(|e| {
e.title("About").description(format!(
"\
I'm a general purpose discord bot written in rusty Rust. \
My main focus is providing information about all kinds of stuff and playing music.\
Use `{}help` to get an overview of the commands I provide.",
std::env::var("BOT_PREFIX").unwrap()
))
})
})
.await?;
Ok(())
}

@ -0,0 +1,37 @@
use crate::utils::context_data::get_database_from_context;
use bot_coreutils::url;
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("Adds media to the database")]
#[usage("<url> [<category>] [<name>]")]
#[bucket("general")]
#[aliases("add_gif", "add-gif", "addgif", "add-media", "addmedia")]
#[min_args(1)]
#[max_args(3)]
#[owners_only]
async fn add_media(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let url = args.single::<String>()?;
if !url::is_valid(&url) {
msg.reply(ctx, "Invalid url").await?;
return Ok(());
}
let category = args.single_quoted::<String>().ok();
let name = args.single_quoted::<String>().ok();
let database = get_database_from_context(&ctx).await;
database.add_media(url, category, name).await?;
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |c| {
c.reference_message(msg)
.content("Media entry added to the database.")
})
.await?;
Ok(())
}

@ -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(())
}

@ -1,6 +1,5 @@
use std::collections::HashSet;
use crate::commands::common::handle_autodelete;
use serenity::client::Context;
use serenity::framework::standard::macros::help;
use serenity::framework::standard::{help_commands, Args};
@ -8,6 +7,8 @@ use serenity::framework::standard::{CommandGroup, CommandResult, HelpOptions};
use serenity::model::channel::Message;
use serenity::model::id::UserId;
use crate::commands::common::handle_autodelete;
#[help]
#[max_levenshtein_distance(2)]
pub async fn help(
@ -18,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(())
}

@ -0,0 +1,18 @@
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;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
#[command]
#[description("Displays a list of all gifs used by the bot")]
#[bucket("general")]
#[only_in(guilds)]
async fn media(ctx: &Context, msg: &Message) -> CommandResult {
let database = get_database_from_context(ctx).await;
let gifs = database.get_all_media().await?;
create_media_menu(ctx, msg.channel_id, gifs, msg.author.id).await?;
Ok(())
}

@ -1,24 +1,41 @@
use serenity::framework::standard::macros::group;
use pekofy::PEKOFY_COMMAND;
use about::ABOUT_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 sauce::SAUCE_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_media;
mod clear;
mod fuck;
pub(crate) mod help;
mod pekofy;
mod inspirobot;
mod media;
mod pain;
mod party;
mod ping;
mod qalc;
mod sauce;
mod shutdown;
mod stats;
mod time;
mod timezones;
mod xkcd;
#[group]
#[commands(ping, stats, shutdown, pekofy, time, timezones, qalc, sauce)]
#[commands(
ping, stats, shutdown, time, timezones, qalc, about, add_media, media, pain, clear, xkcd, fuck,
party, inspirobot
)]
pub struct Misc;

@ -0,0 +1,42 @@
use crate::utils::context_data::get_database_from_context;
use crate::utils::error::BotError;
use rand::prelude::IteratorRandom;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
static CATEGORY_PREFIX: &str = "pain-";
static NOT_FOUND_PAIN: &str = "404";
#[command]
#[description("Various types of pain (pain-peko)")]
#[usage("<pain-type>")]
#[example("peko")]
#[min_args(1)]
#[max_args(1)]
#[bucket("general")]
async fn pain(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
tracing::debug!("Got pain command");
let pain_type = args.message().to_lowercase();
let database = get_database_from_context(ctx).await;
let mut media = database
.get_media_by_category(format!("{}{}", CATEGORY_PREFIX, pain_type))
.await?;
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 entry = media
.into_iter()
.choose(&mut rand::thread_rng())
.ok_or(BotError::from("No gifs found."))?;
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(())
}

@ -6,6 +6,7 @@ use serenity::model::channel::Message;
#[command]
#[description("Simple ping test command")]
#[usage("")]
#[bucket("general")]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
msg.reply(ctx, "Pong!").await?;

@ -1,10 +1,11 @@
use crate::providers::qalc;
use regex::Regex;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use crate::providers::qalc;
static QALC_HELP: &[&str] = &["help", "--help", "-h", "h"];
#[command]
@ -12,6 +13,8 @@ static QALC_HELP: &[&str] = &["help", "--help", "-h", "h"];
#[min_args(1)]
#[usage("<expression>")]
#[example("1 * 1 + 1 / sqrt(2)")]
#[aliases("calc", "calculate", "qalculate")]
#[bucket("general")]
async fn qalc(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let expression = args.message();
lazy_static::lazy_static! {

@ -1,60 +0,0 @@
use crate::messages::sauce::show_sauce_menu;
use crate::utils::get_previous_message_or_reply;
use sauce_api::Sauce;
use crate::utils::context_data::Store;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
#[command]
#[description("Searches for the source of a previously posted image or an image replied to.")]
#[usage("")]
async fn sauce(ctx: &Context, msg: &Message) -> CommandResult {
log::debug!("Got sauce command");
let source_msg = get_previous_message_or_reply(ctx, msg).await?;
if source_msg.is_none() {
log::debug!("No source message provided");
msg.channel_id.say(ctx, "No source message found.").await?;
return Ok(());
}
let source_msg = source_msg.unwrap();
log::trace!("Source message is {:?}", source_msg);
let mut attachment_urls: Vec<String> =
source_msg.attachments.into_iter().map(|a| a.url).collect();
let mut embed_images = source_msg
.embeds
.into_iter()
.filter_map(|e| e.thumbnail)
.map(|t| t.url)
.collect::<Vec<String>>();
attachment_urls.append(&mut embed_images);
log::trace!("Image urls {:?}", attachment_urls);
if attachment_urls.is_empty() {
log::debug!("No images in source image");
msg.channel_id.say(ctx, "Images in message found.").await?;
return Ok(());
}
log::debug!(
"Checking SauceNao for {} attachments",
attachment_urls.len()
);
let data = ctx.data.read().await;
let store_data = data.get::<Store>().unwrap();
let sources = store_data.sauce_nao.check_sauces(attachment_urls).await?;
log::trace!("Sources are {:?}", sources);
log::debug!("Creating menu...");
show_sauce_menu(ctx, msg, sources).await?;
log::debug!("Menu created");
Ok(())
}

@ -1,19 +1,21 @@
use crate::commands::common::handle_autodelete;
use std::process;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use serenity::prelude::*;
use std::process;
use crate::commands::common::handle_autodelete;
#[command]
#[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,24 +1,28 @@
use crate::commands::common::handle_autodelete;
use std::process;
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 std::process;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use sysinfo::{ProcessExt, SystemExt};
use sysinfo::{Pid, PidExt, ProcessExt, SystemExt};
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
use crate::commands::common::handle_autodelete;
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();
@ -26,20 +30,33 @@ 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();
let discord_info = format!(
r#"
Version: {}
Compiled with: rustc {}
Build at: {}
Owner: <@{}>
Guilds: {}
Shards: {}
Voice Connections: {}
Times Used: {}
"#,
VERSION, bot_info.owner.id, guild_count
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,
get_queue_count(ctx).await,
total_commands_executed
);
log::trace!("Discord info {}", discord_info);
tracing::trace!("Discord info {}", discord_info);
let system_info = format!(
r#"
@ -57,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| {
@ -72,3 +89,11 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
Ok(())
}
/// Returns the total number of queues that are not
/// flagged to leave
async fn get_queue_count(ctx: &Context) -> usize {
let data = ctx.data.read().await;
let players = data.get::<MusicPlayers>().unwrap();
players.len()
}

@ -9,20 +9,21 @@ 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());
let first_timezone = args.single::<String>().ok();
let second_timezone = args.single::<String>().ok();
let from_timezone: Tz = if let Some(first) = &first_timezone {
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 {
second.parse::<Tz>()?
crate::forward_error!(ctx, msg.channel_id, second.parse::<Tz>())
} else {
Tz::UTC
};
@ -32,18 +33,25 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
} else {
let now = Utc::now();
if second_timezone.is_some() {
from_timezone.datetime_from_str(
&format!("{} {}:00", now.format("%Y-%m-%d"), &*when),
"%Y-%m-%d %H:%M:%S",
)?
crate::forward_error!(
ctx,
msg.channel_id,
from_timezone.datetime_from_str(
&format!("{} {}:00", now.format("%Y-%m-%d"), &*when),
"%Y-%m-%d %H:%M:%S",
)
)
} else {
let timezone: Tz = "UTC".parse().unwrap();
timezone
.datetime_from_str(
crate::forward_error!(
ctx,
msg.channel_id,
timezone.datetime_from_str(
&format!("{} {}:00", now.format("%Y-%m-%d"), &*when),
"%Y-%m-%d %H:%M:%S",
)?
.with_timezone(&from_timezone)
)
)
.with_timezone(&from_timezone)
}
};

@ -8,6 +8,7 @@ use serenity::model::channel::Message;
#[min_args(1)]
#[usage("<query...>")]
#[example("Europe Berlin")]
#[bucket("general")]
async fn timezones(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let query = args
.iter::<String>()

@ -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(())
}

@ -3,9 +3,11 @@ pub use misc::help::HELP;
pub use misc::MISC_GROUP;
pub use music::MUSIC_GROUP;
pub use settings::SETTINGS_GROUP;
pub use weeb::WEEB_GROUP;
mod common;
pub(crate) mod minecraft;
pub(crate) mod misc;
pub(crate) mod music;
pub(crate) mod settings;
pub(crate) mod weeb;

@ -1,34 +1,41 @@
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 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("Clears the queue")]
#[usage("")]
#[aliases("cl")]
#[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 = 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();
}
msg.channel_id
.say(ctx, "The queue has been cleared")
.await?;
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("🧹 The queue has been cleared")
})
.await?;
handle_autodelete(ctx, msg).await?;
Ok(())

@ -1,34 +1,39 @@
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::messages::music::NowPlayingMessage;
use std::mem;
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]
#[only_in(guilds)]
#[description("Displays the currently playing song")]
#[usage("")]
#[aliases("nowplaying", "np")]
#[bucket("general")]
async fn current(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
log::debug!("Displaying current song for queue in {}", guild.id);
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let mut queue_lock = queue.lock().await;
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 mut player = player.lock().await;
player.queue().current().clone()
};
if let Some(current) = queue_lock.current() {
let metadata = current.metadata().clone();
log::trace!("Metadata is {:?}", metadata);
let np_msg =
NowPlayingMessage::create(ctx.http.clone(), &msg.channel_id, &metadata).await?;
if let Some(old_np) = mem::replace(&mut queue_lock.now_playing_msg, Some(np_msg)) {
let _ = old_np.inner().delete().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?;

@ -1,20 +1,52 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::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, join_channel};
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)]
#[description("Joins a voice channel")]
#[usage("")]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let 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;
#[bucket("general")]
async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
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 {
crate::forward_error!(
ctx,
msg.channel_id,
get_channel_for_author(&msg.author.id, &guild)
)
}
} else {
crate::forward_error!(
ctx,
msg.channel_id,
get_channel_for_author(&msg.author.id, &guild)
)
};
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")
})
.await?;
handle_autodelete(ctx, msg).await?;
Ok(())

@ -4,42 +4,46 @@ use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_queue_for_guild, get_voice_manager, is_dj};
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)]
#[description("Leaves a voice channel")]
#[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 = get_queue_for_guild(ctx, &guild.id).await?;
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();
}
if let Some(current) = queue_lock.current() {
current.stop()?;
handler_lock.leave().await?;
}
if manager.get(guild.id).is_some() {
manager.remove(guild.id).await?;
msg.channel_id.say(ctx, "Left the voice channel").await?;
log::debug!("Left the voice channel");
} else {
msg.channel_id.say(ctx, "Not in a voice channel").await?;
log::debug!("Not in a voice channel");
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?;
handle_autodelete(ctx, msg).await?;
Ok(())

@ -1,50 +1,55 @@
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)]
#[description("Shows the lyrics for the currently playing song")]
#[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 = 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,55 +1,60 @@
use std::sync::Arc;
use crate::providers::music::queue::{MusicQueue, Song};
use crate::providers::music::youtube_dl;
use crate::utils::context_data::{DatabaseContainer, Store};
use crate::utils::error::{BotError, BotResult};
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 songbird::{
Call, Event, EventContext, EventHandler as VoiceEventHandler, Songbird, TrackEvent,
};
use std::mem;
use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering};
use std::time::Duration;
use serenity::model::user::User;
use songbird::Songbird;
use tokio::sync::Mutex;
use youtube_metadata::get_video_information;
mod clear_queue;
mod current;
mod join;
mod leave;
mod lyrics;
mod pause;
mod play;
mod play_next;
mod playlists;
mod queue;
mod save_playlist;
mod shuffle;
mod skip;
use crate::providers::settings::{get_setting, Setting};
use clear_queue::CLEAR_QUEUE_COMMAND;
use current::CURRENT_COMMAND;
use join::JOIN_COMMAND;
use leave::LEAVE_COMMAND;
use lyrics::LYRICS_COMMAND;
use move_song::MOVE_SONG_COMMAND;
use pause::PAUSE_COMMAND;
use play::PLAY_COMMAND;
use play_next::PLAY_NEXT_COMMAND;
use playlists::PLAYLISTS_COMMAND;
use queue::QUEUE_COMMAND;
use remove_song::REMOVE_SONG_COMMAND;
use save_playlist::SAVE_PLAYLIST_COMMAND;
use serenity::model::user::User;
use shuffle::SHUFFLE_COMMAND;
use skip::SKIP_COMMAND;
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, MusicPlayers, Store};
use crate::utils::error::{BotError, BotResult};
mod clear_queue;
mod current;
mod join;
mod leave;
mod lyrics;
mod move_song;
mod pause;
mod play;
mod play_next;
mod playlists;
mod queue;
mod remove_song;
mod save_playlist;
mod shuffle;
mod skip;
#[group]
#[commands(
join,
@ -64,120 +69,18 @@ use skip::SKIP_COMMAND;
pause,
save_playlist,
playlists,
lyrics
lyrics,
move_song,
remove_song
)]
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
@ -186,80 +89,18 @@ fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<Channe
.voice_states
.get(author_id)
.and_then(|voice_state| voice_state.channel_id)
.ok_or(BotError::from("Not in a voice channel."))
}
/// Returns the voice manager from the context
async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> {
songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone()
.ok_or(BotError::from("You're not in a Voice Channel"))
}
/// 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 queue = store
.music_queues
.get(guild_id)
.ok_or(BotError::from("No queue for server"))?
.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);
let players = data.get::<MusicPlayers>().unwrap();
if let Some(np) = &mut queue_lock.now_playing_msg {
let _ = np.refresh(track.metadata()).await;
}
queue_lock.set_current(track);
} else {
if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) {
let _ = np.inner().delete().await;
}
queue_lock.clear_current();
}
true
players.get(&guild_id.0).cloned()
}
/// Returns the list of songs for a given url
@ -279,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?
@ -305,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);
@ -372,9 +241,31 @@ 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
async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool> {
pub async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool> {
let dj_role = get_setting::<String>(ctx, guild, Setting::MusicDjRole).await?;
if let Some(role_name) = dj_role {
@ -390,3 +281,27 @@ 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)
}
}

@ -0,0 +1,48 @@
use crate::commands::common::handle_autodelete;
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, 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")]
#[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).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);
};
{
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!(
"↕ Moved Song `{}` to new position `{}`",
pos1, pos2
))
})
.await?;
handle_autodelete(ctx, msg).await?;
Ok(())
}

@ -1,34 +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::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 = 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");
msg.channel_id.say(ctx, "Paused playback").await?;
if is_paused {
tracing::debug!("Paused");
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("⏸️ Paused playback")
})
.await?;
player.update_now_playing().await?;
} else {
log::debug!("Resumed");
msg.channel_id.say(ctx, "Resumed playback").await?;
tracing::debug!("Resumed");
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("▶ Resumed playback")
})
.await?;
player.update_now_playing().await?;
}
} else {
msg.channel_id.say(ctx, "Nothing to pause").await?;

@ -1,64 +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::commands::common::handle_autodelete;
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 = handler.ok_or(CommandError::from("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,57 +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")]
#[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 = handler.ok_or(CommandError::from("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?;

@ -1,17 +1,19 @@
use crate::commands::common::handle_autodelete;
use crate::utils::context_data::get_database_from_context;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::utils::context_data::get_database_from_context;
#[command]
#[only_in(guilds)]
#[description("Displays a list of all saved playlists")]
#[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,31 +1,59 @@
use std::cmp::min;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::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;
#[command]
#[only_in(guilds)]
#[description("Shows the song queue")]
#[usage("")]
#[usage("(<query...>)")]
#[aliases("q")]
async fn queue(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::trace!("Displaying queue for guild {}", guild.id);
#[bucket("general")]
async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
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 = get_queue_for_guild(ctx, &guild.id).await?;
let queue_lock = queue.lock().await;
let songs: Vec<(usize, String)> = 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()
.map(|s| s.title().clone())
.enumerate()
.filter(|(i, s)| {
if query.is_empty() {
return true;
}
for kw in &query {
if s.title().to_lowercase().contains(kw)
|| s.author().to_lowercase().contains(kw)
|| &i.to_string() == kw
{
return true;
}
}
false
})
.map(|(i, s)| (i, s.clone()))
.collect();
log::trace!("Songs are {:?}", songs);
tracing::trace!("Songs are {:?}", songs);
if songs.len() == 0 {
msg.channel_id
@ -36,26 +64,8 @@ async fn queue(ctx: &Context, msg: &Message) -> CommandResult {
return Ok(());
}
create_queue_menu(ctx, msg.channel_id, songs).await?;
let mut song_list = Vec::new();
for i in 0..min(10, songs.len() - 1) {
song_list.push(format!("{:0>3} - {}", songs[i].0 + 1, songs[i].1))
}
if songs.len() > 10 {
song_list.push("...".to_string());
let last = songs.last().unwrap();
song_list.push(format!("{:0>3} - {}", last.0 + 1, last.1))
}
log::trace!("Song list is {:?}", song_list);
msg.channel_id
.send_message(ctx, |m| {
m.embed(|e| {
e.title("Queue")
.description(format!("```\n{}\n```", song_list.join("\n")))
})
})
.await?;
handle_autodelete(ctx, msg).await?;
Ok(())

@ -0,0 +1,45 @@
use crate::commands::common::handle_autodelete;
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, 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")]
#[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).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);
};
{
let mut player = player.lock().await;
player.queue().remove(pos);
}
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content(format!("🗑️ Removed Song at `{}`", pos))
})
.await?;
handle_autodelete(ctx, msg).await?;
Ok(())
}

@ -1,27 +1,26 @@
use crate::commands::music::is_dj;
use crate::utils::context_data::get_database_from_context;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
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,

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

Loading…
Cancel
Save