Compare commits

...

29 Commits

Author SHA1 Message Date
trivernis 9bfb35dbce
Fix yt-dlp
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/tag/container Pipeline was successful Details
5 months ago
trivernis 048d6a5cba
Merge branch 'main' of ssh://git.trivernis.net:22321/Trivernis/2b-rs 5 months ago
trivernis b2b09a3f4f
Update animethemes-rs
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/tag/container Pipeline was successful Details
5 months ago
trivernis e3250e1bc6
Add forgejo id to container build task
ci/woodpecker/tag/container Pipeline was successful Details
5 months ago
trivernis 2d869f3ea8
Update dependency to fix build
ci/woodpecker/tag/container Pipeline failed Details
5 months ago
trivernis e6726e41c8
Update README
ci/woodpecker/push/build Pipeline failed Details
ci/woodpecker/tag/container Pipeline failed Details
5 months ago
trivernis bc57733dbf
Fix build workflow
ci/woodpecker/push/build Pipeline failed Details
5 months ago
trivernis 37b22a7d51
Install clippy in build workflow
ci/woodpecker/push/build Pipeline failed Details
5 months ago
trivernis 6c9889e3e1
Increment version
ci/woodpecker/push/build Pipeline failed Details
5 months ago
trivernis 8836f311c9
Add two woodpecker workflows 5 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

@ -3,7 +3,8 @@ name: Build Container
on:
workflow_dispatch:
push:
branches: [ main, actions ]
tags:
- "v*"
schedule:
# daily builds to always include patches in the docker image
- cron: '0 4 * * *'

@ -1,4 +1,4 @@
name: Build and Test
name: Check
on:
workflow_dispatch:
@ -31,9 +31,15 @@ jobs:
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Run Rustfmt
uses: actions-rust-lang/rustfmt@v1.0.0
- name: Build
run: cargo build --verbose
- name: Run Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Test coreutils
run: cargo test --verbose --package bot-coreutils

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

3426
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,9 +1,9 @@
[workspace]
members=["bot-coreutils", "bot-database", "."]
members=["bot-coreutils", "bot-database", "bot-database/migration", "."]
[package]
name = "tobi-rs"
version = "0.9.13"
version = "0.11.4"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
@ -15,43 +15,41 @@ panic = 'abort'
[dependencies]
bot-database = {path="./bot-database"}
bot-coreutils = {path="./bot-coreutils"}
serenity = "0.10.10"
serenity = "0.11.5"
dotenv = "0.15.0"
serde_derive = "1.0.136"
serde = "1.0.136"
thiserror = "1.0.30"
minecraft-data-rs = "0.4.5"
songbird = "0.2.2"
serde_json = "1.0.79"
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.5.5"
regex = "1.6.0"
aspotify = "0.7.1"
lazy_static = "1.4.0"
futures = "0.3.21"
chrono = "0.4.19"
sysinfo = "0.23.8"
reqwest = "0.11.10"
chrono-tz = "0.6.1"
sauce-api = "0.9.3"
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.1.5"
typemap_rev = "0.2.0"
youtube-metadata = "0.2.0"
xkcd-search = "0.1.2"
animethemes-rs = "0.3.0"
build-time = "0.1.1"
tracing-subscriber = {version = "0.3.10", features = ["env-filter"] }
tracing = "0.1.32"
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.serenity-rich-interaction]
[dependencies.songbird]
version = "0.3.0"
features = ["yt-dlp"]
[dependencies.tokio]
version = "1.17.0"
version = "1.21.2"
features = ["macros", "rt-multi-thread"]
[dependencies.lavalink-rs]
git = "https://gitlab.com/vicky5124/lavalink-rs/"
rev = "2487c295"
features=["native", "serenity", "songbird", "tracing-log"]
default-features = false
# [patch.crates-io]
# serenity-additions = { path = "../serenity-additions" }

@ -4,7 +4,7 @@ FROM ${BASE_IMAGE} AS build_base
RUN apk update
RUN apk add --no-cache --force-overwrite \
build-base \
openssl3-dev \
openssl-dev \
libopusenc-dev \
libpq-dev \
curl \
@ -22,23 +22,24 @@ COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY bot-coreutils ./bot-coreutils
COPY bot-database ./bot-database
RUN cargo build --release --verbose
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 \
openssl3 \
openssl \
libopusenc \
libpq \
python3 \
py3-pip \
qalc \
ffmpeg \
bash
RUN pip3 install youtube-dl
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"]
ENTRYPOINT ["/tobi-rs"]

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

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

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

@ -8,10 +8,13 @@ edition = "2018"
[dependencies]
dotenv = "0.15.0"
chrono = "0.4.19"
thiserror = "1.0.30"
diesel = { version = "1.4.8", features = ["postgres", "r2d2", "chrono"] }
log = "0.4.16"
diesel_migrations = "1.4.0"
r2d2 = "0.8.9"
tokio-diesel = {git = "https://github.com/Trivernis/tokio-diesel"}
chrono = "0.4.22"
thiserror = "1.0.37"
tracing = "0.1.37"
[dependencies.sea-orm]
version = "0.9.3"
features = ["runtime-tokio-native-tls", "sqlx-postgres"]
[dependencies.migration]
path = "./migration"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,2 +0,0 @@
-- Your SQL goes here
ALTER TABLE gifs RENAME TO media;

@ -1,56 +1,49 @@
use sea_orm::ActiveValue::Set;
use std::time::SystemTime;
use diesel::prelude::*;
use diesel::{delete, insert_into};
use tokio_diesel::*;
use crate::entity::ephemeral_messages;
use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::Database;
use sea_orm::prelude::*;
impl Database {
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<()> {
use ephemeral_messages::dsl;
insert_into(dsl::ephemeral_messages)
.values(EphemeralMessageInsert {
channel_id: channel_id as i64,
message_id: message_id as i64,
timeout,
})
.execute_async(&self.pool)
.await?;
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
pub async fn get_ephemeral_messages(&self) -> DatabaseResult<Vec<EphemeralMessage>> {
use ephemeral_messages::dsl;
let messages: Vec<EphemeralMessage> = dsl::ephemeral_messages
.load_async::<EphemeralMessage>(&self.pool)
.await?;
#[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<()> {
use ephemeral_messages::dsl;
delete(dsl::ephemeral_messages)
.filter(dsl::channel_id.eq(channel_id))
.filter(dsl::message_id.eq(message_id))
.execute_async(&self.pool)
ephemeral_messages::Entity::delete_many()
.filter(ephemeral_messages::Column::ChannelId.eq(channel_id))
.filter(ephemeral_messages::Column::MessageId.eq(message_id))
.exec(&self.db)
.await?;
Ok(())

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

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

@ -1,59 +1,47 @@
use diesel::insert_into;
use diesel::prelude::*;
use tokio_diesel::*;
use crate::entity::media;
use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::Database;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
use std::fmt::Debug;
impl Database {
impl super::BotDatabase {
/// Returns a list of all gifs in the database
pub async fn get_all_media(&self) -> DatabaseResult<Vec<Media>> {
use media::dsl;
log::debug!("Loading all gifs from 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?;
let gifs: Vec<Media> = dsl::media.load_async::<Media>(&self.pool).await?;
Ok(gifs)
Ok(entries)
}
/// Returns a list of gifs by assigned category
pub async fn get_media_by_category<S: AsRef<str> + 'static>(
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_media_by_category<S: AsRef<str> + 'static + Debug>(
&self,
category: S,
) -> DatabaseResult<Vec<Media>> {
use media::dsl;
log::debug!("Searching for gifs in category '{}'", category.as_ref());
let gifs: Vec<Media> = dsl::media
.filter(dsl::category.eq(category.as_ref()))
.load_async::<Media>(&self.pool)
) -> DatabaseResult<Vec<media::Model>> {
let entries = media::Entity::find()
.filter(media::Column::Category.eq(category.as_ref()))
.all(&self.db)
.await?;
Ok(gifs)
Ok(entries)
}
/// Adds a gif to the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_media(
&self,
url: &str,
url: String,
category: Option<String>,
name: Option<String>,
) -> DatabaseResult<()> {
use media::dsl;
log::debug!(
"Inserting gif with url '{}' and name {:?} and category {:?}",
url,
name,
category
);
insert_into(dsl::media)
.values(MediaInsert {
url: url.to_string(),
name,
category,
})
.execute_async(&self.pool)
.await?;
let model = media::ActiveModel {
url: Set(url),
category: Set(category),
name: Set(name),
..Default::default()
};
model.insert(&self.db).await?;
Ok(())
}

@ -2,11 +2,10 @@ pub use ephemeral_messages::*;
pub use guild_playlists::*;
pub use guild_playlists::*;
pub use media::*;
use sea_orm::DatabaseConnection;
pub use statistics::*;
pub use youtube_songs::*;
use crate::PoolConnection;
mod ephemeral_messages;
mod guild_playlists;
mod guild_settings;
@ -15,16 +14,12 @@ mod statistics;
mod youtube_songs;
#[derive(Clone)]
pub struct Database {
pool: PoolConnection,
pub struct BotDatabase {
db: DatabaseConnection,
}
unsafe impl Send for Database {}
unsafe impl Sync for Database {}
impl Database {
pub fn new(pool: PoolConnection) -> Self {
Self { pool }
impl BotDatabase {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
}

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

@ -1,69 +1,56 @@
use diesel::prelude::*;
use diesel::{delete, insert_into};
use tokio_diesel::*;
use crate::entity::youtube_songs;
use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::Database;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
impl Database {
impl super::BotDatabase {
/// 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: &str,
artist: &str,
title: &str,
album: &str,
url: &str,
spotify_id: String,
artist: String,
title: String,
album: String,
url: String,
) -> DatabaseResult<()> {
use youtube_songs::dsl;
log::debug!(
"Inserting/Updating song in database spotify_id: '{}' artist: '{}', title: '{}', album: '{}', url: '{}'",
spotify_id,
artist,
title,
album,
url,
);
insert_into(dsl::youtube_songs)
.values(YoutubeSongInsert {
spotify_id: spotify_id.to_string(),
artist: artist.to_string(),
title: title.to_string(),
album: album.to_string(),
url: url.to_string(),
})
.on_conflict((dsl::spotify_id, dsl::url))
.do_update()
.set(dsl::score.eq(dsl::score + 1))
.execute_async(&self.pool)
.await?;
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
pub async fn get_song(&self, spotify_id: &str) -> DatabaseResult<Option<YoutubeSong>> {
use youtube_songs::dsl;
let songs: Vec<YoutubeSong> = dsl::youtube_songs
.filter(dsl::spotify_id.eq(spotify_id))
.order(dsl::score.desc())
.limit(1)
.load_async::<YoutubeSong>(&self.pool)
#[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(songs.into_iter().next())
Ok(song)
}
/// Deletes a song from the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn delete_song(&self, id: i64) -> DatabaseResult<()> {
use youtube_songs::dsl;
delete(dsl::youtube_songs)
.filter(dsl::id.eq(id))
.execute_async(&self.pool)
youtube_songs::Entity::delete_many()
.filter(youtube_songs::Column::Id.eq(id))
.exec(&self.db)
.await?;
Ok(())

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

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

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

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

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

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

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

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

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

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

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

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

@ -10,13 +10,13 @@ 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_rich_interaction::RegisterRichInteractions;
use serenity_additions::RegisterAdditions;
use songbird::SerenityInit;
use crate::commands::*;
use crate::handler::{get_raw_event_handler, Handler};
use crate::utils;
use crate::utils::context_data::{
get_database_from_context, DatabaseContainer, MusicPlayers, Store, StoreData,
};
@ -24,25 +24,16 @@ use crate::utils::error::{BotError, BotResult};
pub async fn get_client() -> BotResult<Client> {
let token = env::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?;
let database = get_database()?;
let client = Client::builder(token)
.register_rich_interactions_with(get_raw_event_handler())
let database = get_database().await?;
let client = Client::builder(token, GatewayIntents::all())
.register_serenity_additions_with(get_raw_event_handler())
.event_handler(Handler)
.framework(get_framework().await)
.register_songbird()
.type_map_insert::<Store>(StoreData::new())
.type_map_insert::<Store>(StoreData::create().await)
.type_map_insert::<DatabaseContainer>(database)
.type_map_insert::<MusicPlayers>(HashMap::new())
.await?;
let data = client.data.clone();
let current_application = client
.cache_and_http
.http
.get_current_application_info()
.await?;
utils::initialize_lavalink(data, current_application).await?;
Ok(client)
}
@ -107,8 +98,8 @@ async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Command
let database = get_database_from_context(ctx).await;
let _ = database
.add_statistic(
crate::VERSION,
cmd_name,
crate::VERSION.to_string(),
cmd_name.to_string(),
SystemTime::now(),
error_msg.is_none(),
error_msg,
@ -124,7 +115,7 @@ async fn before_hook(ctx: &Context, msg: &Message, _: &str) -> bool {
}
#[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 {
@ -140,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 } => {

@ -4,8 +4,8 @@ use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[description("Adds media to the database")]
@ -26,7 +26,7 @@ async fn add_media(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
let name = args.single_quoted::<String>().ok();
let database = get_database_from_context(&ctx).await;
database.add_media(&url, category, name).await?;
database.add_media(url, category, name).await?;
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |c| {
c.reference_message(msg)
.content("Media entry added to the database.")

@ -1,11 +1,12 @@
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_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[description("Clears the chat (maximum 100 messages)")]

@ -2,7 +2,6 @@ use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use serenity::model::id::UserId;
use std::time::Duration;
#[command]
@ -13,7 +12,6 @@ use std::time::Duration;
#[bucket("general")]
#[aliases("frick", "fock")]
async fn fuck(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let person = args.single::<UserId>()?;
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?")

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

@ -10,7 +10,6 @@ use pain::PAIN_COMMAND;
use party::PARTY_COMMAND;
use ping::PING_COMMAND;
use qalc::QALC_COMMAND;
use reset_lavalink::RESET_LAVALINK_COMMAND;
use shutdown::SHUTDOWN_COMMAND;
use stats::STATS_COMMAND;
use time::TIME_COMMAND;
@ -28,7 +27,6 @@ mod pain;
mod party;
mod ping;
mod qalc;
mod reset_lavalink;
mod shutdown;
mod stats;
mod time;
@ -37,21 +35,7 @@ mod xkcd;
#[group]
#[commands(
ping,
stats,
shutdown,
time,
timezones,
qalc,
about,
add_media,
media,
pain,
clear,
xkcd,
fuck,
party,
inspirobot,
reset_lavalink
ping, stats, shutdown, time, timezones, qalc, about, add_media, media, pain, clear, xkcd, fuck,
party, inspirobot
)]
pub struct Misc;

@ -1,40 +0,0 @@
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::providers::music::lavalink::Lavalink;
use crate::utils::initialize_lavalink;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use std::mem;
use std::sync::Arc;
#[command]
#[description("Resets the lavalink connection")]
#[aliases("reconnect_lavalink", "reset-lavalink", "reconnect-lavalink")]
#[num_args(0)]
#[owners_only]
async fn reset_lavalink(ctx: &Context, msg: &Message) -> CommandResult {
let app_info = ctx.http.get_current_application_info().await?;
destroy_lavalink(ctx).await;
initialize_lavalink(Arc::clone(&ctx.data), app_info).await?;
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("Reconnected to lavalink")
})
.await?;
handle_autodelete(ctx, msg).await?;
Ok(())
}
async fn destroy_lavalink(ctx: &Context) {
let mut data = ctx.data.write().await;
{
let lava_client = data.remove::<Lavalink>().unwrap();
mem::drop(lava_client);
}
}

@ -33,7 +33,7 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
let uptime = own_process.run_time();
let uptime = ChronoDuration::from_std(Duration::from_secs(uptime)).unwrap();
let total_commands_executed = database.get_total_commands_statistic().await?;
let shard_count = ctx.cache.shard_count().await;
let shard_count = ctx.cache.shard_count();
let discord_info = format!(
r#"

@ -6,8 +6,8 @@ use serenity::model::channel::Message;
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_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -17,7 +17,7 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[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);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await {

@ -15,7 +15,7 @@ use crate::messages::music::now_playing::create_now_playing_msg;
#[aliases("nowplaying", "np")]
#[bucket("general")]
async fn current(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Displaying current song for queue in {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await {

@ -1,62 +0,0 @@
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message;
use serenity::prelude::*;
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_rich_interaction::core::{MEDIUM_TIMEOUT, SHORT_TIMEOUT};
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
#[description("Loads an equalizer preset")]
#[usage("<preset>")]
#[num_args(1)]
#[example("bass")]
#[bucket("general")]
#[checks(DJ)]
async fn equalize(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Changing equalizer for {}", guild.id);
let preset = args.single::<String>().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);
};
let bands = match preset.to_lowercase().as_str() {
"metal" => lavalink_rs::EQ_METAL,
"boost" => lavalink_rs::EQ_BOOST,
"base" => lavalink_rs::EQ_BASE,
"piano" => lavalink_rs::EQ_PIANO,
_ => {
EphemeralMessage::create(&ctx.http, msg.channel_id, MEDIUM_TIMEOUT, |m| {
m.content(format!(
"Unknown preset '{}'. Available are 'metal', 'boost', 'base' and 'piano'",
preset
))
})
.await?;
handle_autodelete(ctx, msg).await?;
return Ok(());
}
};
{
let mut player = player.lock().await;
player.equalize_all(bands).await?;
}
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content(format!("🎛️ Changed equalizer to '{}'", preset))
})
.await?;
handle_autodelete(ctx, msg).await?;
Ok(())
}

@ -1,33 +0,0 @@
use serenity::framework::standard::macros::command;
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_music_player_for_guild, DJ_CHECK};
use crate::messages::music::equalizer::create_equalizer_message;
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
#[command]
#[only_in(guilds)]
#[description("Displays the equalizer for the music player")]
#[usage("")]
#[bucket("general")]
#[checks(DJ)]
async fn equalizer(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Displaying equalizer 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);
};
create_equalizer_message(&ctx, msg.channel_id, player).await?;
handle_autodelete(ctx, msg).await?;
Ok(())
}

@ -7,8 +7,8 @@ use crate::commands::common::handle_autodelete;
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_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -16,7 +16,7 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[usage("")]
#[bucket("general")]
async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
let channel_id = if let Ok(arg) = args.single::<u64>() {
if is_dj(ctx, guild.id, &msg.author).await? {
ChannelId(arg)

@ -6,8 +6,8 @@ use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::DJ_CHECK;
use crate::utils::context_data::MusicPlayers;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -17,13 +17,13 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[bucket("general")]
#[checks(DJ)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Leave request received for guild {}", guild.id);
let manager = songbird::get(ctx).await.unwrap();
if let Some(handler) = manager.get(guild.id) {
let mut handler_lock = handler.lock().await;
let _ = handler_lock.leave().await;
handler_lock.leave().await?;
}
let mut data = ctx.data.write().await;

@ -13,7 +13,7 @@ use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
#[usage("")]
#[bucket("general")]
async fn lyrics(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Fetching lyrics for song playing in {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await {

@ -18,8 +18,6 @@ use youtube_metadata::get_video_information;
use clear_queue::CLEAR_QUEUE_COMMAND;
use current::CURRENT_COMMAND;
use equalize::EQUALIZE_COMMAND;
use equalizer::EQUALIZER_COMMAND;
use join::JOIN_COMMAND;
use leave::LEAVE_COMMAND;
use lyrics::LYRICS_COMMAND;
@ -43,8 +41,6 @@ use crate::utils::error::{BotError, BotResult};
mod clear_queue;
mod current;
mod equalize;
mod equalizer;
mod join;
mod leave;
mod lyrics;
@ -75,9 +71,7 @@ mod skip;
playlists,
lyrics,
move_song,
remove_song,
equalizer,
equalize
remove_song
)]
pub struct Music;
@ -257,7 +251,6 @@ pub async fn check_dj(
) -> Result<(), Reason> {
let guild = msg
.guild(&ctx.cache)
.await
.ok_or(Reason::Log("Not in a guild".to_string()))?;
if is_dj(ctx, guild.id, &msg.author)

@ -5,8 +5,8 @@ use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
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")]
@ -18,7 +18,7 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[aliases("mvs", "movesong", "move-song")]
#[checks(DJ)]
async fn move_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Moving song for guild {}", guild.id);
let pos1 = args.single::<usize>()?;

@ -6,8 +6,8 @@ use serenity::prelude::*;
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_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -16,7 +16,7 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[bucket("general")]
#[checks(DJ)]
async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
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 {

@ -22,7 +22,7 @@ use std::sync::Arc;
async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message();
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Play request received for guild {}", guild.id);
let mut player = get_music_player_for_guild(ctx, guild.id).await;

@ -22,7 +22,7 @@ use std::sync::Arc;
async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message();
let guild = msg.guild(&ctx.cache).await.unwrap();
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;

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

@ -16,7 +16,7 @@ use crate::providers::music::queue::Song;
#[aliases("q")]
#[bucket("general")]
async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::trace!("Displaying queue for guild {}", guild.id);
let query = args

@ -5,8 +5,8 @@ use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[description("Removes a song from the queue")]
@ -18,7 +18,7 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[aliases("rms", "removesong", "remove-song")]
#[checks(DJ)]
async fn remove_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Moving song for guild {}", guild.id);
let pos = args.single::<usize>()?;

@ -16,7 +16,7 @@ use crate::utils::context_data::get_database_from_context;
#[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();
let name: String = args.single().unwrap();
let url: &str = args.remains().unwrap();

@ -6,8 +6,8 @@ use serenity::model::channel::Message;
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_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -17,7 +17,7 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[bucket("general")]
#[checks(DJ)]
async fn shuffle(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Shuffling queue for guild {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await {

@ -6,8 +6,8 @@ use serenity::model::channel::Message;
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_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[only_in(guilds)]
@ -17,7 +17,7 @@ use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[bucket("general")]
#[checks(DJ)]
async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Skipping song for guild {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await {

@ -17,13 +17,13 @@ use crate::utils::context_data::get_database_from_context;
#[bucket("general")]
async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let database = get_database_from_context(ctx).await;
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
tracing::debug!("Displaying guild setting for guild {}", guild.id);
if let Some(key) = args.single::<String>().ok() {
tracing::debug!("Displaying guild setting of '{}'", key);
let setting = database
.get_guild_setting::<String>(guild.id.0, key.clone())
.get_guild_setting::<String, _>(guild.id.0, key.clone())
.await?;
match setting {
@ -47,7 +47,7 @@ async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
{
match database
.get_guild_setting::<String>(guild.id.0, key.clone())
.get_guild_setting::<String, _>(guild.id.0, key.clone())
.await?
{
Some(value) => kv_pairs.push(format!("`{}` = `{}`", key, value)),

@ -28,7 +28,7 @@ async fn set(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
return Ok(());
}
let database = get_database_from_context(ctx).await;
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
if let Ok(value) = args.single::<String>() {
database

@ -3,6 +3,7 @@ use serenity::client::Context;
use serenity::framework::standard::macros::group;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
use std::fmt::Debug;
use hololive::amelia::AMELIA_COMMAND;
use hololive::fubuki::FUBUKI_COMMAND;
@ -37,7 +38,7 @@ mod theme;
pub struct Weeb;
/// Posts a random media entry with the given category
async fn post_random_media<S: AsRef<str> + 'static>(
async fn post_random_media<S: AsRef<str> + 'static + Debug>(
ctx: &Context,
msg: &Message,
category: S,

@ -1,4 +1,4 @@
use sauce_api::Sauce;
use sauce_api::source::Source;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
@ -59,15 +59,20 @@ async fn sauce(ctx: &Context, msg: &Message) -> CommandResult {
);
let data = ctx.data.read().await;
let store_data = data.get::<Store>().unwrap();
let sources = store_data
.sauce_nao
.check_sauces(&attachment_urls[..])
.await?;
tracing::trace!("Sources are {:?}", sources);
let mut outputs = Vec::new();
tracing::debug!("Creating menu...");
for attachment in attachment_urls {
let output = store_data
.sauce_nao
.check(&attachment)
.await
.map_err(|_| "failed to retrieve source")?;
tracing::trace!("Output is {:?}", output);
outputs.push(output)
}
show_sauce_menu(ctx, msg, sources).await?;
tracing::debug!("Creating menu...");
show_sauce_menu(ctx, msg, outputs).await?;
tracing::debug!("Menu created");
Ok(())

@ -1,12 +1,12 @@
use crate::messages::theme::create_theme_menu;
use animethemes_rs::client::AnimeThemesClient;
use animethemes_rs::includes;
use animethemes_rs::includes::{AnimeInclude, SearchIncludes};
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use serenity_rich_interaction::core::MEDIUM_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::MEDIUM_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[description("Query for the opening/ending/insert song of an anime")]
@ -20,12 +20,14 @@ async fn theme(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let search_results = client
.search(
query,
&[includes::ANIME],
&[
"animethemes",
"animethemes.animethemeentries",
"animethemes.animethemeentries.videos",
],
&["anime"],
SearchIncludes {
anime: AnimeInclude::default()
.themes()
.themes_entries()
.themes_entries_videos(),
..Default::default()
},
)
.await?;
if let Some(anime) = search_results.anime {

@ -4,7 +4,7 @@ use serenity::model::channel::GuildChannel;
use serenity::model::event::ResumedEvent;
use serenity::model::gateway::{Activity, Ready};
use serenity::model::guild::Member;
use serenity::model::id::{ChannelId, GuildId};
use serenity::model::id::ChannelId;
use serenity::model::voice::VoiceState;
use serenity::prelude::*;
@ -12,8 +12,8 @@ use crate::commands::music::get_music_player_for_guild;
use crate::utils::context_data::MusicPlayers;
use crate::utils::delete_messages_from_database;
use serenity::model::event;
use serenity_rich_interaction::events::RichEventHandler;
use serenity_rich_interaction::Result;
use serenity_additions::events::RichEventHandler;
use serenity_additions::Result;
/// Returns the raw event handler built from a rich event handler
pub fn get_raw_event_handler() -> RichEventHandler {
@ -34,8 +34,9 @@ async fn ready(ctx: &Context, _: &Ready) -> Result<()> {
tracing::info!("Ready");
delete_messages_from_database(&ctx).await?;
let prefix = std::env::var("BOT_PREFIX").unwrap_or("~!".to_string());
ctx.set_activity(Activity::listening(format!("{}help", prefix).as_str()))
ctx.set_activity(Activity::listening(format!("{prefix}help").as_str()))
.await;
tracing::info!("Fully initialized. Listening to {prefix}help");
Ok(())
}
@ -50,13 +51,12 @@ impl EventHandler for Handler {
async fn voice_state_update(
&self,
ctx: Context,
guild_id: Option<GuildId>,
old_state: Option<VoiceState>,
new_state: VoiceState,
) {
let mut member_count = None;
let guild_id = if let Some(gid) = guild_id {
let guild_id = if let Some(gid) = new_state.guild_id {
gid
} else {
return;
@ -81,7 +81,7 @@ impl EventHandler for Handler {
}
// handle disconnects
if let (Some(state), None) = (old_state, new_state.channel_id) {
let current_user = ctx.cache.current_user().await;
let current_user = ctx.cache.current_user();
if state.user_id == current_user.id {
let mut data = ctx.data.write().await;
@ -101,7 +101,7 @@ impl EventHandler for Handler {
async fn get_own_channel_member_count(ctx: &Context, channel_id: ChannelId) -> Option<usize> {
let guild_channel = get_guild_channel(ctx, channel_id).await?;
let current_user = ctx.cache.current_user().await;
let current_user = ctx.cache.current_user();
let members = guild_channel.members(&ctx).await.ok()?;
let own_channel = members
@ -119,7 +119,7 @@ async fn get_own_channel_member_count(ctx: &Context, channel_id: ChannelId) -> O
/// Returns the guild channel for a guild ID
async fn get_guild_channel(ctx: &Context, channel_id: ChannelId) -> Option<GuildChannel> {
if let Some(channel) = ctx.cache.channel(channel_id).await {
if let Some(channel) = ctx.cache.channel(channel_id) {
return channel.guild();
}
let channel = ctx.http.get_channel(channel_id.0).await.ok()?;

@ -3,7 +3,7 @@ use bot_database::models::Media;
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::id::{ChannelId, UserId};
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use serenity_additions::menu::{MenuBuilder, Page};
use std::time::Duration;
/// Creates a new gifs embed

@ -3,10 +3,8 @@ use crate::utils::error::BotResult;
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::id::{ChannelId, UserId};
use serenity_rich_interaction::core::EXTRA_LONG_TIMEOUT;
use serenity_rich_interaction::menu::{
close_menu, display_page, MenuBuilder, Page, CLOSE_MENU_EMOJI,
};
use serenity_additions::core::EXTRA_LONG_TIMEOUT;
use serenity_additions::menu::{close_menu, display_page, MenuBuilder, Page, CLOSE_MENU_EMOJI};
static REFRESH_EMOJI: &str = "🔄";
@ -30,7 +28,7 @@ pub async fn create_inspirobot_menu(
Box::pin(async {
let message = create_inspirobot_page()
.await
.map_err(|e| serenity_rich_interaction::Error::Msg(format!("{}", e)))?;
.map_err(|e| serenity_additions::Error::Msg(format!("{}", e)))?;
Ok(message)
})
}))

@ -1,16 +1,16 @@
use crate::utils::context_data::get_database_from_context;
use crate::utils::error::BotResult;
use serenity::client::Context;
use serenity_rich_interaction::core::MessageHandle;
use serenity_additions::core::MessageHandle;
use std::time::{Duration, SystemTime};
pub mod gifs;
pub mod inspirobot;
pub mod minecraft;
pub mod music;
pub mod sauce;
pub mod theme;
pub mod xkcd;
pub mod inspirobot;
/// Adds an ephemeral message to the database
pub async fn add_ephemeral_handle_to_database(

@ -1,262 +0,0 @@
use crate::commands::music::is_dj;
use crate::providers::music::player::MusicPlayer;
use crate::utils::error::BotResult;
use serenity::builder::{CreateEmbed, CreateMessage};
use serenity::client::Context;
use serenity::model::channel::Reaction;
use serenity::model::id::ChannelId;
use serenity_rich_interaction::core::EXTRA_LONG_TIMEOUT;
use serenity_rich_interaction::menu::{display_page, Menu, MenuBuilder, Page};
use serenity_rich_interaction::Result as SerenityUtilsResult;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use tokio::sync::Mutex;
use typemap_rev::TypeMapKey;
static DELETE_BUTTON: &str = "🗑️";
static NEXT_BAND_BUTTON: &str = "➡️";
static PREVIOUS_BAND_BUTTON: &str = "⬅️";
static ADD_BUTTON: &str = "";
static SUB_BUTTON: &str = "";
struct SelectedBand;
impl TypeMapKey for SelectedBand {
type Value = Arc<AtomicU8>;
}
struct Player;
impl TypeMapKey for Player {
type Value = Arc<Mutex<MusicPlayer>>;
}
/// Creates a new equalizer message
pub async fn create_equalizer_message(
ctx: &Context,
channel_id: ChannelId,
player: Arc<Mutex<MusicPlayer>>,
) -> BotResult<()> {
let selected_band = Arc::new(AtomicU8::new(0));
let selected_band_clone = Arc::clone(&selected_band);
let player_clone = Arc::clone(&player);
MenuBuilder::default()
.add_page(Page::new_builder(move || {
let player = Arc::clone(&player_clone);
let selected_band = Arc::clone(&selected_band_clone);
Box::pin(async move {
let mut page = CreateMessage::default();
let mut embed = CreateEmbed::default();
create_equalizer_embed(selected_band.load(Ordering::Relaxed), &mut embed, &player)
.await;
page.embed(|e| {
e.0.clone_from(&embed.0);
e
});
Ok(page)
})
}))
.add_control(-1, DELETE_BUTTON, |c, m, r| Box::pin(delete_menu(c, m, r)))
.add_help(DELETE_BUTTON, "Deletes this message.")
.add_control(0, PREVIOUS_BAND_BUTTON, |c, m, r| {
Box::pin(previous_band(c, m, r))
})
.add_help(PREVIOUS_BAND_BUTTON, "Selects the previous band.")
.add_control(1, NEXT_BAND_BUTTON, |c, m, r| Box::pin(next_band(c, m, r)))
.add_help(NEXT_BAND_BUTTON, "Selects the next band.")
.add_control(3, ADD_BUTTON, |c, m, r| Box::pin(add_to_band(c, m, r)))
.add_help(ADD_BUTTON, "Adds to the selected band.")
.add_control(2, SUB_BUTTON, |c, m, r| {
Box::pin(subtract_from_band(c, m, r))
})
.add_help(SUB_BUTTON, "Subtracts from the selected band")
.show_help()
.add_data::<SelectedBand>(selected_band)
.add_data::<Player>(player)
.timeout(EXTRA_LONG_TIMEOUT)
.build(ctx, channel_id)
.await?;
Ok(())
}
/// Creates a new equalizer embed
async fn create_equalizer_embed<'a>(
selected_band: u8,
embed: &'a mut CreateEmbed,
player: &Arc<Mutex<MusicPlayer>>,
) -> &'a mut CreateEmbed {
let mut description = String::new();
let bands = {
let player = player.lock().await;
player.get_equalizer().clone()
};
for i in 0..bands.len() {
if i as u8 == selected_band {
description += "⤋"
} else {
description += " ";
}
}
description += "\n";
for i in (0..11).rev() {
let eq_value = (i as f64) / 20.0 - 0.25;
for band in &bands {
if (eq_value > 0. && band >= &eq_value) || (eq_value < 0. && band <= &eq_value) {
description += "█";
} else if eq_value == 0. {
description += format!("-").as_str();
} else {
description += " ";
}
}
description += "\n";
}
for i in 0..bands.len() {
if i as u8 == selected_band {
description += "⤊"
} else {
description += " ";
}
}
embed
.title("Equalizer")
.description(format!("```\n{}\n```", description));
embed
}
/// Selects the previous band
async fn next_band(
ctx: &Context,
menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
let selected_band = menu.data.get::<SelectedBand>().unwrap();
if selected_band.load(Ordering::SeqCst) >= 14 {
selected_band.store(0, Ordering::SeqCst);
} else {
selected_band.fetch_add(1, Ordering::SeqCst);
}
display_page(ctx, menu).await?;
Ok(())
}
/// Selects the previous band
async fn previous_band(
ctx: &Context,
menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
let selected_band = menu.data.get::<SelectedBand>().unwrap();
if selected_band.load(Ordering::SeqCst) <= 0 {
selected_band.store(14, Ordering::SeqCst);
} else {
selected_band.fetch_sub(1, Ordering::SeqCst);
}
display_page(ctx, menu).await?;
Ok(())
}
/// Adds to the selected band
async fn add_to_band(
ctx: &Context,
menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
{
let selected_band = menu
.data
.get::<SelectedBand>()
.unwrap()
.load(Ordering::Relaxed);
let player = menu.data.get::<Player>().unwrap();
let mut player = player.lock().await;
let equalizer = player.get_equalizer();
let current_value = equalizer[selected_band as usize];
if current_value < 0.25 {
player.equalize(selected_band, current_value + 0.05).await?;
}
}
display_page(ctx, menu).await?;
Ok(())
}
/// Substracts from the selected band
async fn subtract_from_band(
ctx: &Context,
menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
{
let selected_band = menu
.data
.get::<SelectedBand>()
.unwrap()
.load(Ordering::Relaxed);
let player = menu.data.get::<Player>().unwrap();
let mut player = player.lock().await;
let equalizer = player.get_equalizer();
let current_value = equalizer[selected_band as usize];
if current_value > -0.25 {
player.equalize(selected_band, current_value - 0.05).await?;
}
}
display_page(ctx, menu).await?;
Ok(())
}
/// Deletes the menu
async fn delete_menu(
ctx: &Context,
menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
let handle = menu.message.read().await;
ctx.http
.delete_message(handle.channel_id, handle.message_id)
.await?;
Ok(())
}

@ -1,4 +1,3 @@
pub mod equalizer;
pub mod no_voicechannel;
pub mod now_playing;
pub mod queue;

@ -1,8 +1,8 @@
use crate::utils::error::BotResult;
use serenity::http::Http;
use serenity::model::prelude::ChannelId;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
use std::sync::Arc;
/// Creates a not in a voicechannel message

@ -14,9 +14,9 @@ use crate::utils::error::*;
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::channel::Reaction;
use serenity_rich_interaction::core::MessageHandle;
use serenity_rich_interaction::menu::{Menu, MenuBuilder, Page};
use serenity_rich_interaction::Result as SerenityUtilsResult;
use serenity_additions::core::MessageHandle;
use serenity_additions::menu::{Menu, MenuBuilder, Page};
use serenity_additions::Result as SerenityUtilsResult;
use std::env;
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};

@ -3,7 +3,7 @@ use crate::utils::error::BotResult;
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::id::ChannelId;
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use serenity_additions::menu::{MenuBuilder, Page};
use std::time::Duration;
/// Creates a new queue menu

@ -1,6 +1,5 @@
use std::cmp::Ordering;
use sauce_api::{SauceItem, SauceResult};
use serenity::builder::CreateMessage;
use serenity::{model::channel::Message, prelude::*};
@ -8,7 +7,7 @@ use bot_coreutils::url::get_domain_for_url;
use crate::utils::error::BotResult;
use rand::prelude::SliceRandom;
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use serenity_additions::menu::{MenuBuilder, Page};
use std::time::Duration;
static MAX_RESULTS: usize = 6;
@ -24,7 +23,7 @@ static SAUCE_MESSAGES: &[&str] = &[
pub async fn show_sauce_menu(
ctx: &Context,
msg: &Message,
sources: Vec<SauceResult>,
sources: Vec<sauce_api::source::Output>,
) -> BotResult<()> {
let pages: Vec<Page> = sources.into_iter().map(create_sauce_page).collect();
@ -47,7 +46,7 @@ pub async fn show_sauce_menu(
}
/// Creates a single sauce page
fn create_sauce_page<'a>(mut result: SauceResult) -> Page<'a> {
fn create_sauce_page<'a>(mut result: sauce_api::source::Output) -> Page<'a> {
let mut message = CreateMessage::default();
let mut description_lines = Vec::new();
let original = result.original_url;
@ -65,7 +64,7 @@ fn create_sauce_page<'a>(mut result: SauceResult) -> Page<'a> {
});
// display with descending similarity
result.items.reverse();
let items: Vec<(usize, SauceItem)> = result
let items: Vec<(usize, sauce_api::source::Item)> = result
.items
.into_iter()
.filter(|i| i.similarity >= MIN_SIMILARITY)

@ -3,8 +3,8 @@ use animethemes_rs::models::{Anime, ThemeEntry, ThemeType};
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::id::{ChannelId, UserId};
use serenity_rich_interaction::core::EXTRA_LONG_TIMEOUT;
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use serenity_additions::core::EXTRA_LONG_TIMEOUT;
use serenity_additions::menu::{MenuBuilder, Page};
/// Creates a new Anime Theme Menu
pub async fn create_theme_menu(
@ -55,6 +55,11 @@ fn create_theme_pages(anime_entries: Vec<Anime>, nsfw: bool) -> Vec<Page<'static
}
}
}
if pages.is_empty() {
let mut message = CreateMessage::default();
message.embed(|e| e.description("No themes found!"));
pages.push(Page::Static(message));
}
pages
}

@ -2,8 +2,8 @@ use crate::utils::error::BotResult;
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::id::{ChannelId, UserId};
use serenity_rich_interaction::core::LONG_TIMEOUT;
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use serenity_additions::core::LONG_TIMEOUT;
use serenity_additions::menu::{MenuBuilder, Page};
use xkcd_search::Comic;
/// Creates a new xkcd menu

@ -1,4 +1,4 @@
pub(crate) mod minecraft;
pub(crate) mod music;
pub(crate) mod qalc;
pub(crate) mod settings;
pub(crate) mod minecraft;

@ -1,53 +0,0 @@
use crate::utils::context_data::MusicPlayers;
use lavalink_rs::gateway::LavalinkEventHandler;
use lavalink_rs::model::{PlayerUpdate, Stats, TrackFinish, TrackStart};
use lavalink_rs::LavalinkClient;
use serenity::async_trait;
use serenity::prelude::TypeMapKey;
use std::sync::Arc;
use tokio::sync::RwLock;
use typemap_rev::TypeMap;
pub struct LavalinkHandler {
pub data: Arc<RwLock<TypeMap>>,
}
#[async_trait]
impl LavalinkEventHandler for LavalinkHandler {
async fn track_start(&self, _client: LavalinkClient, event: TrackStart) {
tracing::info!("Track started!\nGuild: {}", event.guild_id);
}
async fn track_finish(&self, _: LavalinkClient, event: TrackFinish) {
tracing::info!("Track finished!\nGuild: {}", event.guild_id);
let player = {
let data = self.data.read().await;
let players = data.get::<MusicPlayers>().unwrap();
players.get(&event.guild_id.0).cloned()
};
if let Some(player) = player {
let mut player = player.lock().await;
if let Err(e) = player.play_next().await {
tracing::error!("Failed to play next song: {:?}", e);
}
if let Err(e) = player.update_now_playing().await {
tracing::error!("Failed to update now playing embed: {:?}", e);
}
}
}
async fn player_update(&self, _: LavalinkClient, event: PlayerUpdate) {
tracing::debug!("Received player update event: {:?}", event);
}
async fn stats(&self, _: LavalinkClient, event: Stats) {
tracing::debug!("Received stats event: {:?}", event);
}
}
pub struct Lavalink;
impl TypeMapKey for Lavalink {
type Value = Arc<LavalinkClient>;
}

@ -8,9 +8,9 @@ use responses::VideoInformation;
use youtube_dl::search_video_information;
pub mod inspirobot;
pub mod lavalink;
pub mod lyrics;
pub mod player;
pub mod player_events;
pub mod queue;
pub mod responses;
pub mod spotify;
@ -73,7 +73,7 @@ pub async fn add_youtube_song_to_database(
if let Some(id) = track.id {
database
.add_song(&id, &artists, &track.name, &track.album.name, &url)
.add_song(id, artists, track.name, track.album.name, url)
.await?;
}

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

Loading…
Cancel
Save