Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
14913deb3a | ||
|
fc3b3e08f1 | ||
|
444c5dce33 | ||
|
6bd815fd38 | ||
|
5364e41560 | ||
|
e632f55b4e | ||
|
cd5651c7f6 | ||
|
4c17286614 | ||
|
48b50f783d | ||
|
605bc37db6 | ||
|
bec92177cb | ||
|
cee578eaf1 | ||
|
6615e05196 | ||
|
6cfdc10a6a | ||
|
d3e00247bd | ||
|
6d324e10cb | ||
|
8390bf0ec6 | ||
|
e6f5db1842 | ||
|
fca080253f | ||
6482af923b | |||
e875038851 | |||
|
92d8d077df | ||
|
b861f6f093 | ||
|
66f45f11f2 | ||
|
e30a08e019 | ||
|
80f45a1f5c | ||
|
1a1b1b8144 | ||
34d5fddf6c | |||
ac41dbce0c | |||
208d169c76 | |||
|
9cfcb0d09c | ||
|
cc55b3e1d1 | ||
|
a9a08e656f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
|
.idea
|
||||||
|
2
.idea/.gitignore
vendored
2
.idea/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/workspace.xml
|
|
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="dataSourceStorageLocal" created-in="CL-223.8836.42">
|
|
||||||
<data-source name="MySQL for 5.1 - soundfx@localhost" uuid="1067c1d0-1386-4a39-b3f5-6d48d6f279eb">
|
|
||||||
<database-info product="" version="" jdbc-version="" driver-name="" driver-version="" dbms="MYSQL" exact-version="0" />
|
|
||||||
<secret-storage>master_key</secret-storage>
|
|
||||||
<user-name>jude</user-name>
|
|
||||||
<schema-mapping />
|
|
||||||
</data-source>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
|
||||||
<data-source source="LOCAL" name="MySQL for 5.1 - soundfx@localhost" uuid="1067c1d0-1386-4a39-b3f5-6d48d6f279eb">
|
|
||||||
<driver-ref>mysql</driver-ref>
|
|
||||||
<synchronize>true</synchronize>
|
|
||||||
<jdbc-driver>com.mysql.jdbc.Driver</jdbc-driver>
|
|
||||||
<jdbc-url>jdbc:mysql://localhost:3306/soundfx</jdbc-url>
|
|
||||||
</data-source>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,7 +0,0 @@
|
|||||||
<component name="ProjectDictionaryState">
|
|
||||||
<dictionary name="jude">
|
|
||||||
<words>
|
|
||||||
<w>reqwest</w>
|
|
||||||
</words>
|
|
||||||
</dictionary>
|
|
||||||
</component>
|
|
@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="RsBorrowChecker" enabled="false" level="ERROR" enabled_by_default="false" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="JavaScriptSettings">
|
|
||||||
<option name="languageLevel" value="ES6" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/soundfx-rs.iml" filepath="$PROJECT_DIR$/.idea/soundfx-rs.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="CPP_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="SqlDialectMappings">
|
|
||||||
<file url="PROJECT" dialect="MySQL" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
3374
Cargo.lock
generated
3374
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
34
Cargo.toml
34
Cargo.toml
@ -2,35 +2,45 @@
|
|||||||
name = "soundfx-rs"
|
name = "soundfx-rs"
|
||||||
description = "Discord bot for custom sound effects and soundboards"
|
description = "Discord bot for custom sound effects and soundboards"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
version = "1.5.7"
|
version = "1.5.18"
|
||||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
songbird = { version = "0.3", features = ["builtin-queue"] }
|
songbird = { version = "0.4", features = ["builtin-queue"] }
|
||||||
poise = "0.3"
|
poise = "0.6.1-rc1"
|
||||||
sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] }
|
sqlx = { version = "0.7.3", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] }
|
||||||
tokio = { version = "1", features = ["fs", "process", "io-util"] }
|
tokio = { version = "1", features = ["fs", "process", "io-util", "rt-multi-thread"] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
reqwest = "0.11"
|
reqwest = "0.12"
|
||||||
env_logger = "0.10"
|
env_logger = "0.11"
|
||||||
regex = "1.4"
|
regex = "1.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
dashmap = "5.3"
|
dashmap = "6.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
|
prometheus = { version = "0.13.3", optional = true }
|
||||||
|
axum = { version = "0.7.2", optional = true }
|
||||||
|
|
||||||
[patch."https://github.com/serenity-rs/serenity"]
|
[dependencies.symphonia]
|
||||||
serenity = { version = "0.11.5" }
|
version = "0.5"
|
||||||
|
features = ["ogg"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
metrics = ["dep:prometheus", "dep:axum"]
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
|
features = ["metrics"]
|
||||||
depends = "$auto, ffmpeg"
|
depends = "$auto, ffmpeg"
|
||||||
suggests = "mysql-server-8.0"
|
suggests = "mysql-server-8.0"
|
||||||
maintainer-scripts = "debian"
|
maintainer-scripts = "debian"
|
||||||
assets = [
|
assets = [
|
||||||
["target/release/soundfx-rs", "usr/bin/soundfx-rs", "755"],
|
["target/release/soundfx-rs", "usr/bin/soundfx-rs", "755"],
|
||||||
["conf/default.env", "etc/soundfx-rs/default.env", "600"]
|
["conf/default.env", "etc/soundfx-rs/config.env", "600"]
|
||||||
|
]
|
||||||
|
conf-files = [
|
||||||
|
"/etc/soundfx-rs/config.env",
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.deb.systemd-units]
|
[package.metadata.deb.systemd-units]
|
||||||
|
9
Containerfile
Normal file
9
Containerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
|
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||||
|
CARGO_HOME=/usr/local/cargo \
|
||||||
|
PATH=/usr/local/cargo/bin:$PATH
|
||||||
|
|
||||||
|
RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake ffmpeg libopus-dev pkg-config libssl-dev curl mysql-client-8.0
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal
|
||||||
|
RUN cargo install cargo-deb
|
19
README.md
19
README.md
@ -23,11 +23,26 @@ Options:
|
|||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
1. Install build dependencies: `sudo apt install gcc gcc-multilib cmake ffmpeg libopus-dev`
|
When running from source, the config options above can be configured simply as environment variables.
|
||||||
|
|
||||||
|
Two options for building are offered. The first is easier.
|
||||||
|
|
||||||
|
### Build for local platform
|
||||||
|
|
||||||
|
1. Install build dependencies: `sudo apt install gcc gcc-multilib cmake ffmpeg libopus-dev pkg-config libssl-dev`
|
||||||
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `soundfx`
|
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `soundfx`
|
||||||
3. Install Cargo and Rust from https://rustup.rs
|
3. Install Cargo and Rust from https://rustup.rs
|
||||||
4. Install SQLx CLI: `cargo install sqlx-cli`
|
4. Install SQLx CLI: `cargo install sqlx-cli`
|
||||||
5. From the source code directory, execute `sqlx migrate run`
|
5. From the source code directory, execute `sqlx migrate run`
|
||||||
6. Build with cargo: `cargo build --release`
|
6. Build with cargo: `cargo build --release`
|
||||||
|
|
||||||
When running from source, the config options above can be configured simply as environment variables.
|
### Build for other platform
|
||||||
|
|
||||||
|
By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a different platform. These instructions are written using `podman`, but `docker` should work too.
|
||||||
|
|
||||||
|
1. Install container software: `sudo apt install podman`.
|
||||||
|
2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `soundfx`
|
||||||
|
3. Install SQLx CLI: `cargo install sqlx-cli`
|
||||||
|
4. From the source code directory, execute `sqlx migrate run`
|
||||||
|
5. Build container image: `podman build -t soundfx .`
|
||||||
|
6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/soundfx" soundfx cargo deb`
|
||||||
|
9
debian/postinst
vendored
Executable file
9
debian/postinst
vendored
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
id -u soundfx &>/dev/null || useradd -r -M soundfx
|
||||||
|
|
||||||
|
chown soundfx /etc/soundfx-rs/config.env
|
||||||
|
|
||||||
|
#DEBHELPER#
|
7
debian/postrm
vendored
Normal file
7
debian/postrm
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
id -u soundfx &>/dev/null || userdel soundfx
|
||||||
|
|
||||||
|
#DEBHELPER#
|
6
migrations/20230816145151_favorite_sounds.sql
Normal file
6
migrations/20230816145151_favorite_sounds.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE favorite_sounds (
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
sound_id INT UNSIGNED NOT NULL,
|
||||||
|
FOREIGN KEY (sound_id) REFERENCES `sounds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
PRIMARY KEY (user_id, sound_id)
|
||||||
|
);
|
8
scripts/dump-query.sh
Executable file
8
scripts/dump-query.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
mysql -D soundfx -N -e "SELECT name, hex(src) FROM sounds" > out
|
||||||
|
split --additional-suffix=.row -l 1 out
|
||||||
|
for filename in *.row; do
|
||||||
|
name=`grep -oP '^(.+)(?=\t)' $filename`
|
||||||
|
col=`awk -F '\t' '{print $2}' "$filename"`
|
||||||
|
echo $col > "$filename.hex"
|
||||||
|
xxd -r -p "$filename.hex" "$name.opus"
|
||||||
|
done
|
94
src/cmds/favorite.rs
Normal file
94
src/cmds/favorite.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use crate::{cmds::autocomplete_favorite, models::sound::SoundCtx, Context, Error};
|
||||||
|
|
||||||
|
#[poise::command(slash_command, rename = "favorites", guild_only = true)]
|
||||||
|
pub async fn favorites(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a sound as a favorite
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "add",
|
||||||
|
category = "Favorites",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
|
pub async fn add_favorite(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Name or ID of sound to favorite"] name: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let sounds = ctx
|
||||||
|
.data()
|
||||||
|
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match sounds {
|
||||||
|
Ok(sounds) => {
|
||||||
|
let sound = &sounds[0];
|
||||||
|
|
||||||
|
sound
|
||||||
|
.add_favorite(ctx.author().id, &ctx.data().database)
|
||||||
|
.await?;
|
||||||
|
ctx.say(format!(
|
||||||
|
"Sound {} (ID {}) added to favorites.",
|
||||||
|
sound.name, sound.id
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Couldn't fetch sounds: {:?}", e);
|
||||||
|
|
||||||
|
ctx.say("Failed to find sound.").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a sound from your favorites
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "remove",
|
||||||
|
category = "Favorites",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
|
pub async fn remove_favorite(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Name or ID of sound to favorite"]
|
||||||
|
#[autocomplete = "autocomplete_favorite"]
|
||||||
|
name: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let sounds = ctx
|
||||||
|
.data()
|
||||||
|
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match sounds {
|
||||||
|
Ok(sounds) => {
|
||||||
|
let sound = &sounds[0];
|
||||||
|
|
||||||
|
sound
|
||||||
|
.remove_favorite(ctx.author().id, &ctx.data().database)
|
||||||
|
.await?;
|
||||||
|
ctx.say(format!(
|
||||||
|
"Sound {} (ID {}) removed from favorites.",
|
||||||
|
sound.name, sound.id
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Couldn't fetch sounds: {:?}", e);
|
||||||
|
|
||||||
|
ctx.say("Failed to find sound.").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,23 @@
|
|||||||
|
use poise::{
|
||||||
|
serenity_prelude::{CreateEmbed, CreateEmbedFooter},
|
||||||
|
CreateReply,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{consts::THEME_COLOR, Context, Error};
|
use crate::{consts::THEME_COLOR, Context, Error};
|
||||||
|
|
||||||
/// View bot commands
|
/// View bot commands
|
||||||
#[poise::command(slash_command)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
ctx.send(|m| {
|
ctx.send(
|
||||||
m.ephemeral(true).embed(|e| {
|
CreateReply::default().ephemeral(true).embed(
|
||||||
e.title("Help")
|
CreateEmbed::new()
|
||||||
|
.title("Help")
|
||||||
.color(THEME_COLOR)
|
.color(THEME_COLOR)
|
||||||
.footer(|f| {
|
.footer(CreateEmbedFooter::new(concat!(
|
||||||
f.text(concat!(
|
env!("CARGO_PKG_NAME"),
|
||||||
env!("CARGO_PKG_NAME"),
|
" ver ",
|
||||||
" ver ",
|
env!("CARGO_PKG_VERSION")
|
||||||
env!("CARGO_PKG_VERSION")
|
)))
|
||||||
))
|
|
||||||
})
|
|
||||||
.description(
|
.description(
|
||||||
"__Info Commands__
|
"__Info Commands__
|
||||||
`/help` `/info`
|
`/help` `/info`
|
||||||
@ -33,6 +37,9 @@ __Library Commands__
|
|||||||
`/public` - Set a sound as public/private
|
`/public` - Set a sound as public/private
|
||||||
`/list server` - List sounds on this server
|
`/list server` - List sounds on this server
|
||||||
`/list user` - List your sounds
|
`/list user` - List your sounds
|
||||||
|
`/favorites add` - Add a favorite
|
||||||
|
`/favorites remove` - Remove a favorite
|
||||||
|
`/list favorites` - List favorites
|
||||||
|
|
||||||
__Search Commands__
|
__Search Commands__
|
||||||
`/search` - Search for public sounds by name
|
`/search` - Search for public sounds by name
|
||||||
@ -46,9 +53,9 @@ __Setting Commands__
|
|||||||
|
|
||||||
__Advanced Commands__
|
__Advanced Commands__
|
||||||
`/soundboard` - Create a soundboard",
|
`/soundboard` - Create a soundboard",
|
||||||
)
|
),
|
||||||
})
|
),
|
||||||
})
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -57,15 +64,19 @@ __Advanced Commands__
|
|||||||
/// Get additional information about the bot
|
/// Get additional information about the bot
|
||||||
#[poise::command(slash_command)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let current_user = ctx.discord().cache.current_user();
|
let current_user = ctx.serenity_context().cache.current_user().id.get();
|
||||||
|
|
||||||
ctx.send(|m| m.ephemeral(true)
|
ctx.send(
|
||||||
.embed(|e| e
|
CreateReply::default().ephemeral(true).embed(
|
||||||
.title("Info")
|
CreateEmbed::new()
|
||||||
.color(THEME_COLOR)
|
.title("Info")
|
||||||
.footer(|f| f
|
.color(THEME_COLOR)
|
||||||
.text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION"))))
|
.footer(CreateEmbedFooter::new(concat!(
|
||||||
.description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot
|
env!("CARGO_PKG_NAME"),
|
||||||
|
" ver ",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
)))
|
||||||
|
.description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot
|
||||||
|
|
||||||
**Welcome to SoundFX!**
|
**Welcome to SoundFX!**
|
||||||
Developer: <@203532103185465344>
|
Developer: <@203532103185465344>
|
||||||
@ -73,7 +84,9 @@ Find me on https://discord.jellywx.com/ and on https://github.com/JellyWX :)
|
|||||||
|
|
||||||
**An online dashboard is available!** Visit https://soundfx.jellywx.com/dashboard
|
**An online dashboard is available!** Visit https://soundfx.jellywx.com/dashboard
|
||||||
There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**",
|
There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**",
|
||||||
current_user.id.as_u64())))).await?;
|
current_user)))
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
use poise::serenity_prelude::{Attachment, GuildId, RoleId};
|
use poise::{
|
||||||
use tokio::fs::File;
|
serenity_prelude::{Attachment, CreateAttachment, GuildId, RoleId},
|
||||||
|
CreateReply,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
use crate::metrics::{DELETE_COUNTER, UPLOAD_COUNTER};
|
||||||
use crate::{
|
use crate::{
|
||||||
cmds::autocomplete_sound,
|
cmds::autocomplete_sound,
|
||||||
consts::{MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE},
|
consts::{MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE},
|
||||||
@ -21,6 +25,9 @@ pub async fn upload_new_sound(
|
|||||||
#[description = "Name to upload sound to"] name: String,
|
#[description = "Name to upload sound to"] name: String,
|
||||||
#[description = "Sound file (max. 2MB)"] file: Attachment,
|
#[description = "Sound file (max. 2MB)"] file: Attachment,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
UPLOAD_COUNTER.inc();
|
||||||
|
|
||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
||||||
fn is_numeric(s: &String) -> bool {
|
fn is_numeric(s: &String) -> bool {
|
||||||
@ -35,7 +42,13 @@ pub async fn upload_new_sound(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !name.is_empty() && name.len() <= 20 {
|
if !name.is_empty() && name.len() <= 20 {
|
||||||
if !is_numeric(&name) {
|
if name.starts_with("@") {
|
||||||
|
ctx.say("Sound names cannot start with an @ symbol. Please choose another name")
|
||||||
|
.await?;
|
||||||
|
} else if is_numeric(&name) {
|
||||||
|
ctx.say("Please ensure the sound name contains a non-numerical character")
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
// need to check the name is not currently in use by the user
|
// need to check the name is not currently in use by the user
|
||||||
let count_name =
|
let count_name =
|
||||||
Sound::count_named_user_sounds(ctx.author().id, &name, &ctx.data().database)
|
Sound::count_named_user_sounds(ctx.author().id, &name, &ctx.data().database)
|
||||||
@ -50,14 +63,14 @@ pub async fn upload_new_sound(
|
|||||||
let count = Sound::count_user_sounds(ctx.author().id, &ctx.data().database).await?;
|
let count = Sound::count_user_sounds(ctx.author().id, &ctx.data().database).await?;
|
||||||
let mut permit_upload = true;
|
let mut permit_upload = true;
|
||||||
|
|
||||||
// need to check if user is patreon or nah
|
// need to check if user is Patreon or not
|
||||||
if count >= *MAX_SOUNDS {
|
if count >= *MAX_SOUNDS {
|
||||||
let patreon_guild_member = GuildId(*PATREON_GUILD)
|
let patreon_guild_member = GuildId::from(*PATREON_GUILD)
|
||||||
.member(ctx.discord(), ctx.author().id)
|
.member(ctx, ctx.author().id)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(member) = patreon_guild_member {
|
if let Ok(member) = patreon_guild_member {
|
||||||
permit_upload = member.roles.contains(&RoleId(*PATREON_ROLE));
|
permit_upload = member.roles.contains(&RoleId::from(*PATREON_ROLE));
|
||||||
} else {
|
} else {
|
||||||
permit_upload = false;
|
permit_upload = false;
|
||||||
}
|
}
|
||||||
@ -89,9 +102,6 @@ pub async fn upload_new_sound(
|
|||||||
)).await?;
|
)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ctx.say("Please ensure the sound name contains a non-numerical character")
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.say("Usage: `/upload <name>`. Please ensure the name provided is less than 20 characters in length").await?;
|
ctx.say("Usage: `/upload <name>`. Please ensure the name provided is less than 20 characters in length").await?;
|
||||||
@ -108,10 +118,13 @@ pub async fn delete_sound(
|
|||||||
#[autocomplete = "autocomplete_sound"]
|
#[autocomplete = "autocomplete_sound"]
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
DELETE_COUNTER.inc();
|
||||||
|
|
||||||
let pool = ctx.data().database.clone();
|
let pool = ctx.data().database.clone();
|
||||||
|
|
||||||
let uid = ctx.author().id.0;
|
let uid = ctx.author().id.get();
|
||||||
let gid = ctx.guild_id().unwrap().0;
|
let gid = ctx.guild_id().unwrap().get();
|
||||||
|
|
||||||
let sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
let sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
||||||
let sound_result = sound_vec.first();
|
let sound_result = sound_vec.first();
|
||||||
@ -123,8 +136,8 @@ pub async fn delete_sound(
|
|||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
let has_perms = {
|
let has_perms = {
|
||||||
if let Ok(member) = ctx.guild_id().unwrap().member(&ctx.discord(), uid).await {
|
if let Ok(member) = ctx.guild_id().unwrap().member(&ctx, uid).await {
|
||||||
if let Ok(perms) = member.permissions(&ctx.discord()) {
|
if let Ok(perms) = member.permissions(&ctx) {
|
||||||
perms.manage_guild()
|
perms.manage_guild()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@ -163,8 +176,8 @@ pub async fn change_public(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let pool = ctx.data().database.clone();
|
let pool = ctx.data().database.clone();
|
||||||
|
|
||||||
let uid = ctx.author().id.0;
|
let uid = ctx.author().id.get();
|
||||||
let gid = ctx.guild_id().unwrap().0;
|
let gid = ctx.guild_id().unwrap().get();
|
||||||
|
|
||||||
let mut sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
let mut sound_vec = ctx.data().search_for_sound(&name, gid, uid, true).await?;
|
||||||
let sound_result = sound_vec.first_mut();
|
let sound_result = sound_vec.first_mut();
|
||||||
@ -213,13 +226,13 @@ pub async fn download_file(
|
|||||||
|
|
||||||
match sound.first() {
|
match sound.first() {
|
||||||
Some(sound) => {
|
Some(sound) => {
|
||||||
let source = sound.store_sound_source(&ctx.data().database).await?;
|
|
||||||
|
|
||||||
let file = File::open(&source).await?;
|
|
||||||
let name = format!("{}-{}.opus", sound.id, sound.name);
|
let name = format!("{}-{}.opus", sound.id, sound.name);
|
||||||
|
|
||||||
ctx.send(|m| m.attachment((&file, name.as_str()).into()))
|
ctx.send(CreateReply::default().attachment(CreateAttachment::bytes(
|
||||||
.await?;
|
sound.src(&ctx.data().database).await,
|
||||||
|
name.as_str(),
|
||||||
|
)))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
use poise::serenity_prelude::AutocompleteChoice;
|
||||||
|
|
||||||
use crate::{models::sound::SoundCtx, Context};
|
use crate::{models::sound::SoundCtx, Context};
|
||||||
|
|
||||||
|
pub mod favorite;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod manage;
|
pub mod manage;
|
||||||
pub mod play;
|
pub mod play;
|
||||||
@ -7,18 +10,22 @@ pub mod search;
|
|||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod stop;
|
pub mod stop;
|
||||||
|
|
||||||
pub async fn autocomplete_sound(
|
pub async fn autocomplete_sound(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
|
||||||
ctx: Context<'_>,
|
|
||||||
partial: &str,
|
|
||||||
) -> Vec<poise::AutocompleteChoice<String>> {
|
|
||||||
ctx.data()
|
ctx.data()
|
||||||
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
|
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap_or(vec![])
|
.unwrap_or(vec![])
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| poise::AutocompleteChoice {
|
.map(|s| AutocompleteChoice::new(s.name.clone(), s.id.to_string()))
|
||||||
name: s.name.clone(),
|
.collect()
|
||||||
value: s.id.to_string(),
|
}
|
||||||
})
|
|
||||||
|
pub async fn autocomplete_favorite(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
|
||||||
|
ctx.data()
|
||||||
|
.autocomplete_favorite_sounds(&partial, ctx.author().id)
|
||||||
|
.await
|
||||||
|
.unwrap_or(vec![])
|
||||||
|
.iter()
|
||||||
|
.map(|s| AutocompleteChoice::new(s.name.clone(), s.id.to_string()))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
251
src/cmds/play.rs
251
src/cmds/play.rs
@ -1,11 +1,18 @@
|
|||||||
use poise::serenity_prelude::{
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
builder::CreateActionRow, model::application::component::ButtonStyle, GuildChannel,
|
|
||||||
|
use poise::{
|
||||||
|
serenity_prelude::{
|
||||||
|
builder::CreateActionRow, ButtonStyle, CreateButton, GuildChannel, ReactionType,
|
||||||
|
},
|
||||||
|
CreateReply,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
use crate::metrics::PLAY_COUNTER;
|
||||||
use crate::{
|
use crate::{
|
||||||
cmds::autocomplete_sound,
|
cmds::autocomplete_sound,
|
||||||
models::{guild_data::CtxGuildData, sound::SoundCtx},
|
models::{guild_data::CtxGuildData, sound::SoundCtx},
|
||||||
utils::{join_channel, play_from_query, queue_audio},
|
utils::{join_channel, play_audio, play_from_query, queue_audio},
|
||||||
Context, Error,
|
Context, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -20,27 +27,103 @@ pub async fn play(
|
|||||||
#[channel_types("Voice")]
|
#[channel_types("Voice")]
|
||||||
channel: Option<GuildChannel>,
|
channel: Option<GuildChannel>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
PLAY_COUNTER.inc();
|
||||||
|
|
||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
||||||
let guild = ctx.guild().unwrap();
|
let guild = ctx.guild().map(|g| g.clone()).unwrap();
|
||||||
|
|
||||||
if channel.as_ref().map_or(false, |c| c.is_text_based()) {
|
ctx.say(
|
||||||
ctx.say("The channel specified is not a voice channel.")
|
play_from_query(
|
||||||
.await?;
|
&ctx.serenity_context(),
|
||||||
} else {
|
&ctx.data(),
|
||||||
ctx.say(
|
&guild,
|
||||||
play_from_query(
|
ctx.author().id,
|
||||||
&ctx.discord(),
|
channel.map(|c| c.id),
|
||||||
&ctx.data(),
|
&name,
|
||||||
guild,
|
false,
|
||||||
ctx.author().id,
|
|
||||||
channel.map(|c| c.id),
|
|
||||||
&name,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Play a random sound from this server
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
rename = "random",
|
||||||
|
default_member_permissions = "SPEAK",
|
||||||
|
guild_only = true
|
||||||
|
)]
|
||||||
|
pub async fn play_random(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Channel to play in (default: your current voice channel)"]
|
||||||
|
#[channel_types("Voice")]
|
||||||
|
channel: Option<GuildChannel>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.defer().await?;
|
||||||
|
|
||||||
|
let (channel_to_join, guild_id) = {
|
||||||
|
let guild = ctx.guild().unwrap();
|
||||||
|
|
||||||
|
(
|
||||||
|
channel.map(|c| c.id).or_else(|| {
|
||||||
|
guild
|
||||||
|
.voice_states
|
||||||
|
.get(&ctx.author().id)
|
||||||
|
.and_then(|voice_state| voice_state.channel_id)
|
||||||
|
}),
|
||||||
|
guild.id,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
match channel_to_join {
|
||||||
|
Some(channel) => {
|
||||||
|
let call = join_channel(ctx.serenity_context(), guild_id, channel).await?;
|
||||||
|
|
||||||
|
let sounds = ctx.data().guild_sounds(guild_id, None).await?;
|
||||||
|
if sounds.len() == 0 {
|
||||||
|
ctx.say("No sounds in this server!").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||||
|
|
||||||
|
println!("{}", ts.subsec_micros());
|
||||||
|
|
||||||
|
// This is far cheaper and easier than using an RNG. No reason to use a full RNG here
|
||||||
|
// anyway.
|
||||||
|
match sounds.get(ts.subsec_micros() as usize % sounds.len()) {
|
||||||
|
Some(sound) => {
|
||||||
|
let guild_data = ctx.data().guild_data(guild_id).await.unwrap();
|
||||||
|
let mut lock = call.lock().await;
|
||||||
|
|
||||||
|
play_audio(
|
||||||
|
sound,
|
||||||
|
guild_data.read().await.volume,
|
||||||
|
&mut lock,
|
||||||
|
&ctx.data().database,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
ctx.say(format!("Playing {} (ID {})", sound.name, sound.id))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
ctx.say("No sounds in this server!").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
ctx.say("You are not in a voice chat!").await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -133,24 +216,23 @@ pub async fn queue_play(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
||||||
let guild = ctx.guild().unwrap();
|
let (channel_to_join, guild_id) = {
|
||||||
|
let guild = ctx.guild().unwrap();
|
||||||
|
|
||||||
let channel_to_join = guild
|
(
|
||||||
.voice_states
|
guild
|
||||||
.get(&ctx.author().id)
|
.voice_states
|
||||||
.and_then(|voice_state| voice_state.channel_id);
|
.get(&ctx.author().id)
|
||||||
|
.and_then(|voice_state| voice_state.channel_id),
|
||||||
|
guild.id,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
match channel_to_join {
|
match channel_to_join {
|
||||||
Some(user_channel) => {
|
Some(user_channel) => {
|
||||||
let (call_handler, _) = join_channel(ctx.discord(), guild.clone(), user_channel).await;
|
let call = join_channel(ctx.serenity_context(), guild_id, user_channel).await?;
|
||||||
|
|
||||||
let guild_data = ctx
|
let guild_data = ctx.data().guild_data(guild_id).await.unwrap();
|
||||||
.data()
|
|
||||||
.guild_data(ctx.guild_id().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut lock = call_handler.lock().await;
|
|
||||||
|
|
||||||
let query_terms = [
|
let query_terms = [
|
||||||
Some(sound_1),
|
Some(sound_1),
|
||||||
@ -193,14 +275,18 @@ pub async fn queue_play(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queue_audio(
|
{
|
||||||
&sounds,
|
let mut lock = call.lock().await;
|
||||||
guild_data.read().await.volume,
|
|
||||||
&mut lock,
|
queue_audio(
|
||||||
&ctx.data().database,
|
&sounds,
|
||||||
)
|
guild_data.read().await.volume,
|
||||||
.await
|
&mut lock,
|
||||||
.unwrap();
|
&ctx.data().database,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
ctx.say(format!("Queued {} sounds!", sounds.len())).await?;
|
ctx.say(format!("Queued {} sounds!", sounds.len())).await?;
|
||||||
}
|
}
|
||||||
@ -227,13 +313,13 @@ pub async fn loop_play(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
||||||
let guild = ctx.guild().unwrap();
|
let guild = ctx.guild().map(|g| g.clone()).unwrap();
|
||||||
|
|
||||||
ctx.say(
|
ctx.say(
|
||||||
play_from_query(
|
play_from_query(
|
||||||
&ctx.discord(),
|
&ctx.serenity_context(),
|
||||||
&ctx.data(),
|
&ctx.data(),
|
||||||
guild,
|
&guild,
|
||||||
ctx.author().id,
|
ctx.author().id,
|
||||||
None,
|
None,
|
||||||
&name,
|
&name,
|
||||||
@ -316,21 +402,6 @@ pub async fn soundboard(
|
|||||||
#[description = "Name or ID of sound for button 20"]
|
#[description = "Name or ID of sound for button 20"]
|
||||||
#[autocomplete = "autocomplete_sound"]
|
#[autocomplete = "autocomplete_sound"]
|
||||||
sound_20: Option<String>,
|
sound_20: Option<String>,
|
||||||
#[description = "Name or ID of sound for button 21"]
|
|
||||||
#[autocomplete = "autocomplete_sound"]
|
|
||||||
sound_21: Option<String>,
|
|
||||||
#[description = "Name or ID of sound for button 22"]
|
|
||||||
#[autocomplete = "autocomplete_sound"]
|
|
||||||
sound_22: Option<String>,
|
|
||||||
#[description = "Name or ID of sound for button 23"]
|
|
||||||
#[autocomplete = "autocomplete_sound"]
|
|
||||||
sound_23: Option<String>,
|
|
||||||
#[description = "Name or ID of sound for button 24"]
|
|
||||||
#[autocomplete = "autocomplete_sound"]
|
|
||||||
sound_24: Option<String>,
|
|
||||||
#[description = "Name or ID of sound for button 25"]
|
|
||||||
#[autocomplete = "autocomplete_sound"]
|
|
||||||
sound_25: Option<String>,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
||||||
@ -355,11 +426,6 @@ pub async fn soundboard(
|
|||||||
sound_18,
|
sound_18,
|
||||||
sound_19,
|
sound_19,
|
||||||
sound_20,
|
sound_20,
|
||||||
sound_21,
|
|
||||||
sound_22,
|
|
||||||
sound_23,
|
|
||||||
sound_24,
|
|
||||||
sound_25,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut sounds = vec![];
|
let mut sounds = vec![];
|
||||||
@ -377,24 +443,49 @@ pub async fn soundboard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.send(|m| {
|
let components = {
|
||||||
m.content("**Play a sound:**").components(|c| {
|
let mut c = vec![];
|
||||||
for row in sounds.as_slice().chunks(5) {
|
for row in sounds.as_slice().chunks(5) {
|
||||||
let mut action_row: CreateActionRow = Default::default();
|
let mut action_row = vec![];
|
||||||
for sound in row {
|
for sound in row {
|
||||||
action_row.create_button(|b| {
|
action_row.push(
|
||||||
b.style(ButtonStyle::Primary)
|
CreateButton::new(sound.id.to_string())
|
||||||
.label(&sound.name)
|
.style(ButtonStyle::Primary)
|
||||||
.custom_id(sound.id)
|
.label(&sound.name),
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
c.add_action_row(action_row);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c
|
c.push(CreateActionRow::Buttons(action_row));
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
c.push(CreateActionRow::Buttons(vec![
|
||||||
|
CreateButton::new("#stop")
|
||||||
|
.label("Stop")
|
||||||
|
.emoji(ReactionType::Unicode("⏹".to_string()))
|
||||||
|
.style(ButtonStyle::Danger),
|
||||||
|
CreateButton::new("#mode")
|
||||||
|
.label("Mode:")
|
||||||
|
.style(ButtonStyle::Secondary)
|
||||||
|
.disabled(true),
|
||||||
|
CreateButton::new("#instant")
|
||||||
|
.label("Instant")
|
||||||
|
.emoji(ReactionType::Unicode("▶".to_string()))
|
||||||
|
.style(ButtonStyle::Secondary)
|
||||||
|
.disabled(true),
|
||||||
|
CreateButton::new("#loop")
|
||||||
|
.label("Loop")
|
||||||
|
.emoji(ReactionType::Unicode("🔁".to_string()))
|
||||||
|
.style(ButtonStyle::Secondary),
|
||||||
|
]));
|
||||||
|
|
||||||
|
c
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.send(
|
||||||
|
CreateReply::default()
|
||||||
|
.content("**Play a sound:**")
|
||||||
|
.components(components),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
use poise::{
|
use poise::{
|
||||||
serenity_prelude,
|
serenity_prelude,
|
||||||
serenity_prelude::{
|
serenity_prelude::{
|
||||||
application::component::ButtonStyle,
|
constants::MESSAGE_CODE_LIMIT, ButtonStyle, ComponentInteraction, CreateActionRow,
|
||||||
constants::MESSAGE_CODE_LIMIT,
|
CreateButton, CreateEmbed, EditInteractionResponse, GuildId, UserId,
|
||||||
interaction::{message_component::MessageComponentInteraction, InteractionResponseType},
|
|
||||||
CreateActionRow, CreateEmbed, GuildId, UserId,
|
|
||||||
},
|
},
|
||||||
CreateReply,
|
CreateReply,
|
||||||
};
|
};
|
||||||
@ -16,8 +14,8 @@ use crate::{
|
|||||||
Context, Data, Error,
|
Context, Data, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
|
fn format_search_results(search_results: Vec<Sound>) -> CreateReply {
|
||||||
let mut builder = CreateReply::default();
|
let builder = CreateReply::default();
|
||||||
|
|
||||||
let mut current_character_count = 0;
|
let mut current_character_count = 0;
|
||||||
let title = "Public sounds matching filter:";
|
let title = "Public sounds matching filter:";
|
||||||
@ -32,9 +30,7 @@ fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
|
|||||||
current_character_count <= MESSAGE_CODE_LIMIT - title.len()
|
current_character_count <= MESSAGE_CODE_LIMIT - title.len()
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.embed(|e| e.title(title).fields(field_iter));
|
builder.embed(CreateEmbed::default().title(title).fields(field_iter))
|
||||||
|
|
||||||
builder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show uploaded sounds
|
/// Show uploaded sounds
|
||||||
@ -47,12 +43,14 @@ pub async fn list_sounds(_ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
enum ListContext {
|
enum ListContext {
|
||||||
User = 0,
|
User = 0,
|
||||||
Guild = 1,
|
Guild = 1,
|
||||||
|
Favorite = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListContext {
|
impl ListContext {
|
||||||
pub fn title(&self) -> &'static str {
|
pub fn title(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ListContext::User => "Your sounds",
|
ListContext::User => "Your sounds",
|
||||||
|
ListContext::Favorite => "Your favorite sounds",
|
||||||
ListContext::Guild => "Server sounds",
|
ListContext::Guild => "Server sounds",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,6 +84,20 @@ pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show sounds you have favorited
|
||||||
|
#[poise::command(slash_command, rename = "favorite", guild_only = true)]
|
||||||
|
pub async fn list_favorite_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let pager = SoundPager {
|
||||||
|
nonce: 0,
|
||||||
|
page: 0,
|
||||||
|
context: ListContext::Favorite,
|
||||||
|
};
|
||||||
|
|
||||||
|
pager.reply(ctx).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SoundPager {
|
pub struct SoundPager {
|
||||||
nonce: u64,
|
nonce: u64,
|
||||||
@ -102,15 +114,14 @@ impl SoundPager {
|
|||||||
) -> Result<Vec<Sound>, sqlx::Error> {
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
match self.context {
|
match self.context {
|
||||||
ListContext::User => data.user_sounds(user_id, Some(self.page)).await,
|
ListContext::User => data.user_sounds(user_id, Some(self.page)).await,
|
||||||
|
ListContext::Favorite => data.favorite_sounds(user_id, Some(self.page)).await,
|
||||||
ListContext::Guild => data.guild_sounds(guild_id, Some(self.page)).await,
|
ListContext::Guild => data.guild_sounds(guild_id, Some(self.page)).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_action_row(&self, max_page: u64) -> CreateActionRow {
|
fn create_action_row(&self, max_page: u64) -> CreateActionRow {
|
||||||
let mut row = CreateActionRow::default();
|
let row = CreateActionRow::Buttons(vec![
|
||||||
|
CreateButton::new(
|
||||||
row.create_button(|b| {
|
|
||||||
b.custom_id(
|
|
||||||
serde_json::to_string(&SoundPager {
|
serde_json::to_string(&SoundPager {
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
@ -120,10 +131,8 @@ impl SoundPager {
|
|||||||
)
|
)
|
||||||
.style(ButtonStyle::Primary)
|
.style(ButtonStyle::Primary)
|
||||||
.label("⏪")
|
.label("⏪")
|
||||||
.disabled(self.page == 0)
|
.disabled(self.page == 0),
|
||||||
})
|
CreateButton::new(
|
||||||
.create_button(|b| {
|
|
||||||
b.custom_id(
|
|
||||||
serde_json::to_string(&SoundPager {
|
serde_json::to_string(&SoundPager {
|
||||||
nonce: 1,
|
nonce: 1,
|
||||||
page: self.page.saturating_sub(1),
|
page: self.page.saturating_sub(1),
|
||||||
@ -133,16 +142,12 @@ impl SoundPager {
|
|||||||
)
|
)
|
||||||
.style(ButtonStyle::Secondary)
|
.style(ButtonStyle::Secondary)
|
||||||
.label("◀️")
|
.label("◀️")
|
||||||
.disabled(self.page == 0)
|
.disabled(self.page == 0),
|
||||||
})
|
CreateButton::new("pid")
|
||||||
.create_button(|b| {
|
|
||||||
b.custom_id("pid")
|
|
||||||
.style(ButtonStyle::Success)
|
.style(ButtonStyle::Success)
|
||||||
.label(format!("Page {}", self.page + 1))
|
.label(format!("Page {}", self.page + 1))
|
||||||
.disabled(true)
|
.disabled(true),
|
||||||
})
|
CreateButton::new(
|
||||||
.create_button(|b| {
|
|
||||||
b.custom_id(
|
|
||||||
serde_json::to_string(&SoundPager {
|
serde_json::to_string(&SoundPager {
|
||||||
nonce: 2,
|
nonce: 2,
|
||||||
page: self.page.saturating_add(1),
|
page: self.page.saturating_add(1),
|
||||||
@ -152,10 +157,8 @@ impl SoundPager {
|
|||||||
)
|
)
|
||||||
.style(ButtonStyle::Secondary)
|
.style(ButtonStyle::Secondary)
|
||||||
.label("▶️")
|
.label("▶️")
|
||||||
.disabled(self.page == max_page)
|
.disabled(self.page == max_page),
|
||||||
})
|
CreateButton::new(
|
||||||
.create_button(|b| {
|
|
||||||
b.custom_id(
|
|
||||||
serde_json::to_string(&SoundPager {
|
serde_json::to_string(&SoundPager {
|
||||||
nonce: 3,
|
nonce: 3,
|
||||||
page: max_page,
|
page: max_page,
|
||||||
@ -165,16 +168,14 @@ impl SoundPager {
|
|||||||
)
|
)
|
||||||
.style(ButtonStyle::Primary)
|
.style(ButtonStyle::Primary)
|
||||||
.label("⏩")
|
.label("⏩")
|
||||||
.disabled(self.page == max_page)
|
.disabled(self.page == max_page),
|
||||||
});
|
]);
|
||||||
|
|
||||||
row
|
row
|
||||||
}
|
}
|
||||||
|
|
||||||
fn embed(&self, sounds: &[Sound], count: u64) -> CreateEmbed {
|
fn embed(&self, sounds: &[Sound], count: u64) -> CreateEmbed {
|
||||||
let mut embed = CreateEmbed::default();
|
CreateEmbed::default()
|
||||||
|
|
||||||
embed
|
|
||||||
.color(THEME_COLOR)
|
.color(THEME_COLOR)
|
||||||
.title(self.context.title())
|
.title(self.context.title())
|
||||||
.description(format!("**{}** sounds:", count))
|
.description(format!("**{}** sounds:", count))
|
||||||
@ -188,15 +189,13 @@ impl SoundPager {
|
|||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}));
|
}))
|
||||||
|
|
||||||
embed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_interaction(
|
pub async fn handle_interaction(
|
||||||
ctx: &serenity_prelude::Context,
|
ctx: &serenity_prelude::Context,
|
||||||
data: &Data,
|
data: &Data,
|
||||||
interaction: &MessageComponentInteraction,
|
interaction: &ComponentInteraction,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let user_id = interaction.user.id;
|
let user_id = interaction.user.id;
|
||||||
let guild_id = interaction.guild_id.unwrap();
|
let guild_id = interaction.guild_id.unwrap();
|
||||||
@ -205,18 +204,17 @@ impl SoundPager {
|
|||||||
let sounds = pager.get_page(data, user_id, guild_id).await?;
|
let sounds = pager.get_page(data, user_id, guild_id).await?;
|
||||||
let count = match pager.context {
|
let count = match pager.context {
|
||||||
ListContext::User => data.count_user_sounds(user_id).await?,
|
ListContext::User => data.count_user_sounds(user_id).await?,
|
||||||
|
ListContext::Favorite => data.count_favorite_sounds(user_id).await?,
|
||||||
ListContext::Guild => data.count_guild_sounds(guild_id).await?,
|
ListContext::Guild => data.count_guild_sounds(guild_id).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
interaction
|
interaction
|
||||||
.create_interaction_response(&ctx, |r| {
|
.edit_response(
|
||||||
r.kind(InteractionResponseType::UpdateMessage)
|
&ctx,
|
||||||
.interaction_response_data(|d| {
|
EditInteractionResponse::default()
|
||||||
d.ephemeral(true)
|
.add_embed(pager.embed(&sounds, count))
|
||||||
.add_embed(pager.embed(&sounds, count))
|
.components(vec![pager.create_action_row(count / 25)]),
|
||||||
.components(|c| c.add_action_row(pager.create_action_row(count / 25)))
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -228,6 +226,7 @@ impl SoundPager {
|
|||||||
.await?;
|
.await?;
|
||||||
let count = match self.context {
|
let count = match self.context {
|
||||||
ListContext::User => ctx.data().count_user_sounds(ctx.author().id).await?,
|
ListContext::User => ctx.data().count_user_sounds(ctx.author().id).await?,
|
||||||
|
ListContext::Favorite => ctx.data().count_favorite_sounds(ctx.author().id).await?,
|
||||||
ListContext::Guild => {
|
ListContext::Guild => {
|
||||||
ctx.data()
|
ctx.data()
|
||||||
.count_guild_sounds(ctx.guild_id().unwrap())
|
.count_guild_sounds(ctx.guild_id().unwrap())
|
||||||
@ -235,14 +234,12 @@ impl SoundPager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.send(|r| {
|
ctx.send(
|
||||||
r.ephemeral(true)
|
CreateReply::default()
|
||||||
.embed(|e| {
|
.ephemeral(true)
|
||||||
*e = self.embed(&sounds, count);
|
.embed(self.embed(&sounds, count))
|
||||||
e
|
.components(vec![self.create_action_row(count / 25)]),
|
||||||
})
|
)
|
||||||
.components(|c| c.add_action_row(self.create_action_row(count / 25)))
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -265,36 +262,7 @@ pub async fn search_sounds(
|
|||||||
.search_for_sound(&query, ctx.guild_id().unwrap(), ctx.author().id, false)
|
.search_for_sound(&query, ctx.guild_id().unwrap(), ctx.author().id, false)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
ctx.send(|m| {
|
ctx.send(format_search_results(search_results)).await?;
|
||||||
*m = format_search_results(search_results);
|
|
||||||
m
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show a page of random sounds
|
|
||||||
#[poise::command(slash_command, rename = "random", guild_only = true)]
|
|
||||||
pub async fn show_random_sounds(ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
let search_results = sqlx::query_as_unchecked!(
|
|
||||||
Sound,
|
|
||||||
"
|
|
||||||
SELECT name, id, public, server_id, uploader_id
|
|
||||||
FROM sounds
|
|
||||||
WHERE public = 1
|
|
||||||
ORDER BY rand()
|
|
||||||
LIMIT 25
|
|
||||||
"
|
|
||||||
)
|
|
||||||
.fetch_all(&ctx.data().database)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ctx.send(|m| {
|
|
||||||
*m = format_search_results(search_results);
|
|
||||||
m
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use poise::serenity_prelude::{GuildId, User};
|
use poise::{
|
||||||
|
serenity_prelude::{GuildId, User},
|
||||||
|
CreateReply,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cmds::autocomplete_sound,
|
cmds::autocomplete_sound,
|
||||||
@ -60,16 +63,14 @@ pub async fn set_guild_greet_sound(
|
|||||||
#[description = "User to set join sound for"] user: User,
|
#[description = "User to set join sound for"] user: User,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if user.id != ctx.author().id {
|
if user.id != ctx.author().id {
|
||||||
let guild = ctx.guild().unwrap();
|
let permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache());
|
||||||
let permissions = guild
|
|
||||||
.member_permissions(&ctx.discord(), ctx.author().id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if permissions.map_or(true, |p| !p.manage_guild()) {
|
if permissions.map_or(true, |p| !p.manage_guild()) {
|
||||||
ctx.send(|b| {
|
ctx.send(
|
||||||
b.ephemeral(true)
|
CreateReply::default()
|
||||||
.content("Only admins can change other user's greet sounds.")
|
.ephemeral(true)
|
||||||
})
|
.content("Only admins can change other user's greet sounds."),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -109,16 +110,14 @@ pub async fn unset_guild_greet_sound(
|
|||||||
#[description = "User to set join sound for"] user: User,
|
#[description = "User to set join sound for"] user: User,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if user.id != ctx.author().id {
|
if user.id != ctx.author().id {
|
||||||
let guild = ctx.guild().unwrap();
|
let permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache());
|
||||||
let permissions = guild
|
|
||||||
.member_permissions(&ctx.discord(), ctx.author().id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if permissions.map_or(true, |p| !p.manage_guild()) {
|
if permissions.map_or(true, |p| !p.manage_guild()) {
|
||||||
ctx.send(|b| {
|
ctx.send(
|
||||||
b.ephemeral(true)
|
CreateReply::default()
|
||||||
.content("Only admins can change other user's greet sounds.")
|
.ephemeral(true)
|
||||||
})
|
.content("Only admins can change other user's greet sounds."),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -159,20 +158,19 @@ pub async fn set_user_greet_sound(
|
|||||||
.update_join_sound(ctx.author().id, None::<GuildId>, Some(sound.id))
|
.update_join_sound(ctx.author().id, None::<GuildId>, Some(sound.id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
ctx.send(|b| {
|
ctx.send(CreateReply::default().ephemeral(true).content(format!(
|
||||||
b.ephemeral(true).content(format!(
|
"Greet sound has been set to {} (ID {})",
|
||||||
"Greet sound has been set to {} (ID {})",
|
sound.name, sound.id
|
||||||
sound.name, sound.id
|
)))
|
||||||
))
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
ctx.send(|b| {
|
ctx.send(
|
||||||
b.ephemeral(true)
|
CreateReply::default()
|
||||||
.content("Could not find a sound by that name.")
|
.ephemeral(true)
|
||||||
})
|
.content("Could not find a sound by that name."),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,8 +185,12 @@ pub async fn unset_user_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
.update_join_sound(ctx.author().id, None::<GuildId>, None)
|
.update_join_sound(ctx.author().id, None::<GuildId>, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
ctx.send(|b| b.ephemeral(true).content("Greet sound has been unset"))
|
ctx.send(
|
||||||
.await?;
|
CreateReply::default()
|
||||||
|
.ephemeral(true)
|
||||||
|
.content("Greet sound has been unset"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ use crate::{Context, Error};
|
|||||||
guild_only = true
|
guild_only = true
|
||||||
)]
|
)]
|
||||||
pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
let songbird = songbird::get(ctx.serenity_context()).await.unwrap();
|
||||||
let call_opt = songbird.get(ctx.guild_id().unwrap());
|
let call_opt = songbird.get(ctx.guild_id().unwrap());
|
||||||
|
|
||||||
if let Some(call) = call_opt {
|
if let Some(call) = call_opt {
|
||||||
@ -27,7 +27,7 @@ pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
/// Disconnect the bot
|
/// Disconnect the bot
|
||||||
#[poise::command(slash_command, default_member_permissions = "SPEAK", guild_only = true)]
|
#[poise::command(slash_command, default_member_permissions = "SPEAK", guild_only = true)]
|
||||||
pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let songbird = songbird::get(ctx.discord()).await.unwrap();
|
let songbird = songbird::get(ctx.serenity_context()).await.unwrap();
|
||||||
let _ = songbird.leave(ctx.guild_id().unwrap()).await;
|
let _ = songbird.leave(ctx.guild_id().unwrap()).await;
|
||||||
|
|
||||||
ctx.say("👍").await?;
|
ctx.say("👍").await?;
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
use std::{collections::HashMap, env};
|
|
||||||
|
|
||||||
use poise::serenity_prelude::{
|
use poise::serenity_prelude::{
|
||||||
model::{
|
ActionRowComponent, ButtonKind, Context, CreateActionRow, CreateButton,
|
||||||
application::interaction::{Interaction, InteractionResponseType},
|
EditInteractionResponse, FullEvent, Interaction,
|
||||||
channel::Channel,
|
|
||||||
},
|
|
||||||
utils::shard_id,
|
|
||||||
Activity, Context,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
use crate::metrics::GREET_COUNTER;
|
||||||
use crate::{
|
use crate::{
|
||||||
cmds::search::SoundPager,
|
cmds::search::SoundPager,
|
||||||
models::{
|
models::{
|
||||||
@ -20,140 +16,176 @@ use crate::{
|
|||||||
Data, Error,
|
Data, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
|
pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(), Error> {
|
||||||
match event {
|
match event {
|
||||||
poise::Event::Ready { .. } => {
|
FullEvent::VoiceStateUpdate { old, new, .. } => {
|
||||||
ctx.set_activity(Activity::watching("for /play")).await;
|
|
||||||
}
|
|
||||||
poise::Event::GuildCreate { guild, is_new, .. } => {
|
|
||||||
if *is_new {
|
|
||||||
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
|
||||||
let shard_count = ctx.cache.shard_count();
|
|
||||||
let current_shard_id = shard_id(guild.id.as_u64().to_owned(), shard_count);
|
|
||||||
|
|
||||||
let guild_count = ctx
|
|
||||||
.cache
|
|
||||||
.guilds()
|
|
||||||
.iter()
|
|
||||||
.filter(|g| {
|
|
||||||
shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id
|
|
||||||
})
|
|
||||||
.count() as u64;
|
|
||||||
|
|
||||||
let mut hm = HashMap::new();
|
|
||||||
hm.insert("server_count", guild_count);
|
|
||||||
hm.insert("shard_id", current_shard_id);
|
|
||||||
hm.insert("shard_count", shard_count);
|
|
||||||
|
|
||||||
let response = data
|
|
||||||
.http
|
|
||||||
.post(
|
|
||||||
format!(
|
|
||||||
"https://top.gg/api/bots/{}/stats",
|
|
||||||
ctx.cache.current_user_id().as_u64()
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.header("Authorization", token)
|
|
||||||
.json(&hm)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(res) = response {
|
|
||||||
println!("DiscordBots Response: {:?}", res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
poise::Event::VoiceStateUpdate { old, new, .. } => {
|
|
||||||
if let Some(past_state) = old {
|
if let Some(past_state) = old {
|
||||||
if let (Some(guild_id), None) = (past_state.guild_id, new.channel_id) {
|
if let (Some(guild_id), None) = (past_state.guild_id, new.channel_id) {
|
||||||
if let Some(channel_id) = past_state.channel_id {
|
if let Some(channel_id) = past_state.channel_id {
|
||||||
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
let is_okay = ctx
|
||||||
if channel.members(&ctx).await.map(|m| m.len()).unwrap_or(0) <= 1 {
|
.cache
|
||||||
let songbird = songbird::get(ctx).await.unwrap();
|
.channel(channel_id)
|
||||||
|
.map(|c| c.members(&ctx).ok().map(|m| m.len()))
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(0)
|
||||||
|
<= 1;
|
||||||
|
|
||||||
let _ = songbird.remove(guild_id).await;
|
if is_okay {
|
||||||
}
|
let songbird = songbird::get(ctx).await.unwrap();
|
||||||
|
|
||||||
|
songbird.remove(guild_id).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let (Some(guild_id), Some(user_channel)) = (new.guild_id, new.channel_id) {
|
} else if let (Some(guild_id), Some(user_channel)) = (new.guild_id, new.channel_id) {
|
||||||
if let Some(guild) = ctx.cache.guild(guild_id) {
|
let guild_data_opt = data.guild_data(guild_id).await;
|
||||||
let guild_data_opt = data.guild_data(guild.id).await;
|
|
||||||
|
|
||||||
if let Ok(guild_data) = guild_data_opt {
|
if let Ok(guild_data) = guild_data_opt {
|
||||||
let volume;
|
let volume;
|
||||||
let allowed_greets;
|
let allowed_greets;
|
||||||
|
|
||||||
|
{
|
||||||
|
let read = guild_data.read().await;
|
||||||
|
|
||||||
|
volume = read.volume;
|
||||||
|
allowed_greets = read.allow_greets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowed_greets != AllowGreet::Disabled {
|
||||||
|
if let Some(join_id) = data
|
||||||
|
.join_sound(
|
||||||
|
new.user_id,
|
||||||
|
new.guild_id,
|
||||||
|
allowed_greets == AllowGreet::GuildOnly,
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
let read = guild_data.read().await;
|
let mut sound = sqlx::query_as_unchecked!(
|
||||||
|
Sound,
|
||||||
|
"
|
||||||
|
SELECT name, id, public, server_id, uploader_id
|
||||||
|
FROM sounds
|
||||||
|
WHERE id = ?",
|
||||||
|
join_id
|
||||||
|
)
|
||||||
|
.fetch_one(&data.database)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
volume = read.volume;
|
let call = join_channel(&ctx, guild_id, user_channel).await?;
|
||||||
allowed_greets = read.allow_greets;
|
|
||||||
}
|
|
||||||
|
|
||||||
if allowed_greets != AllowGreet::Disabled {
|
#[cfg(feature = "metrics")]
|
||||||
if let Some(join_id) = data
|
GREET_COUNTER.inc();
|
||||||
.join_sound(
|
|
||||||
new.user_id,
|
|
||||||
new.guild_id,
|
|
||||||
allowed_greets == AllowGreet::GuildOnly,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let mut sound = sqlx::query_as_unchecked!(
|
|
||||||
Sound,
|
|
||||||
"
|
|
||||||
SELECT name, id, public, server_id, uploader_id
|
|
||||||
FROM sounds
|
|
||||||
WHERE id = ?
|
|
||||||
",
|
|
||||||
join_id
|
|
||||||
)
|
|
||||||
.fetch_one(&data.database)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (handler, _) = join_channel(&ctx, guild, user_channel).await;
|
play_audio(
|
||||||
|
&mut sound,
|
||||||
play_audio(
|
volume,
|
||||||
&mut sound,
|
&mut call.lock().await,
|
||||||
volume,
|
&data.database,
|
||||||
&mut handler.lock().await,
|
false,
|
||||||
&data.database,
|
)
|
||||||
false,
|
.await
|
||||||
)
|
.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
poise::Event::InteractionCreate { interaction } => match interaction {
|
FullEvent::InteractionCreate { interaction } => match interaction {
|
||||||
Interaction::MessageComponent(component) => {
|
Interaction::Component(component) => {
|
||||||
if let Some(guild_id) = component.guild_id {
|
if let Some(guild_id) = component.guild_id {
|
||||||
if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await {
|
if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await {
|
||||||
} else {
|
} else {
|
||||||
component
|
let mode = component.data.custom_id.as_str();
|
||||||
.create_interaction_response(ctx, |r| {
|
match mode {
|
||||||
r.kind(InteractionResponseType::DeferredUpdateMessage)
|
"#stop" => {
|
||||||
})
|
component.defer(&ctx).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
play_from_query(
|
let songbird = songbird::get(ctx).await.unwrap();
|
||||||
&ctx,
|
let call_opt = songbird.get(guild_id);
|
||||||
&data,
|
|
||||||
guild_id.to_guild_cached(&ctx).unwrap(),
|
if let Some(call) = call_opt {
|
||||||
component.user.id,
|
let mut lock = call.lock().await;
|
||||||
None,
|
|
||||||
&component.data.custom_id,
|
lock.stop();
|
||||||
false,
|
}
|
||||||
)
|
}
|
||||||
.await;
|
|
||||||
|
"#loop" | "#queue" | "#instant" => {
|
||||||
|
let components = {
|
||||||
|
let mut c = vec![];
|
||||||
|
|
||||||
|
for action_row in &component.message.components {
|
||||||
|
let mut row = vec![];
|
||||||
|
// These are always buttons
|
||||||
|
for component in &action_row.components {
|
||||||
|
match component {
|
||||||
|
ActionRowComponent::Button(button) => match &button
|
||||||
|
.data
|
||||||
|
{
|
||||||
|
ButtonKind::Link { .. } => {}
|
||||||
|
ButtonKind::NonLink { custom_id, style } => row
|
||||||
|
.push(
|
||||||
|
CreateButton::new(
|
||||||
|
if custom_id.starts_with('#') {
|
||||||
|
custom_id.to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
custom_id
|
||||||
|
.split('#')
|
||||||
|
.next()
|
||||||
|
.unwrap(),
|
||||||
|
mode
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.label(button.label.clone().unwrap())
|
||||||
|
.emoji(button.emoji.clone().unwrap())
|
||||||
|
.disabled(
|
||||||
|
custom_id == "#mode"
|
||||||
|
|| custom_id == mode,
|
||||||
|
)
|
||||||
|
.style(*style),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.push(CreateActionRow::Buttons(row));
|
||||||
|
}
|
||||||
|
c
|
||||||
|
};
|
||||||
|
|
||||||
|
let response =
|
||||||
|
EditInteractionResponse::default().components(components);
|
||||||
|
|
||||||
|
component.edit_response(&ctx, response).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
id_mode => {
|
||||||
|
component.defer(&ctx).await.unwrap();
|
||||||
|
|
||||||
|
let mut it = id_mode.split('#');
|
||||||
|
let id = it.next().unwrap();
|
||||||
|
let mode = it.next().unwrap_or("instant");
|
||||||
|
|
||||||
|
let guild =
|
||||||
|
guild_id.to_guild_cached(&ctx).map(|g| g.clone()).unwrap();
|
||||||
|
|
||||||
|
play_from_query(
|
||||||
|
&ctx,
|
||||||
|
&data,
|
||||||
|
&guild,
|
||||||
|
component.user.id,
|
||||||
|
None,
|
||||||
|
id.split('#').next().unwrap(),
|
||||||
|
mode == "loop",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
83
src/main.rs
83
src/main.rs
@ -5,6 +5,8 @@ mod cmds;
|
|||||||
mod consts;
|
mod consts;
|
||||||
mod error;
|
mod error;
|
||||||
mod event_handlers;
|
mod event_handlers;
|
||||||
|
#[cfg(feature = "metrics")]
|
||||||
|
mod metrics;
|
||||||
mod models;
|
mod models;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
@ -12,11 +14,11 @@ use std::{env, path::Path, sync::Arc};
|
|||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use poise::serenity_prelude::{
|
use poise::serenity_prelude::{
|
||||||
builder::CreateApplicationCommands,
|
|
||||||
model::{
|
model::{
|
||||||
gateway::GatewayIntents,
|
gateway::GatewayIntents,
|
||||||
id::{GuildId, UserId},
|
id::{GuildId, UserId},
|
||||||
},
|
},
|
||||||
|
ActivityData, ClientBuilder,
|
||||||
};
|
};
|
||||||
use songbird::SerenityInit;
|
use songbird::SerenityInit;
|
||||||
use sqlx::{MySql, Pool};
|
use sqlx::{MySql, Pool};
|
||||||
@ -28,7 +30,6 @@ type Database = MySql;
|
|||||||
|
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
database: Pool<Database>,
|
database: Pool<Database>,
|
||||||
http: reqwest::Client,
|
|
||||||
guild_data_cache: DashMap<GuildId, Arc<RwLock<GuildData>>>,
|
guild_data_cache: DashMap<GuildId, Arc<RwLock<GuildData>>>,
|
||||||
join_sound_cache: DashMap<UserId, DashMap<Option<GuildId>, Option<u32>>>,
|
join_sound_cache: DashMap<UserId, DashMap<Option<GuildId>, Option<u32>>>,
|
||||||
}
|
}
|
||||||
@ -36,40 +37,10 @@ pub struct Data {
|
|||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||||
|
|
||||||
pub async fn register_application_commands(
|
|
||||||
ctx: &poise::serenity_prelude::Context,
|
|
||||||
framework: &poise::Framework<Data, Error>,
|
|
||||||
guild_id: Option<GuildId>,
|
|
||||||
) -> Result<(), poise::serenity_prelude::Error> {
|
|
||||||
let mut commands_builder = CreateApplicationCommands::default();
|
|
||||||
let commands = &framework.options().commands;
|
|
||||||
for command in commands {
|
|
||||||
if let Some(slash_command) = command.create_as_slash_command() {
|
|
||||||
commands_builder.add_application_command(slash_command);
|
|
||||||
}
|
|
||||||
if let Some(context_menu_command) = command.create_as_context_menu_command() {
|
|
||||||
commands_builder.add_application_command(context_menu_command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0);
|
|
||||||
|
|
||||||
if let Some(guild_id) = guild_id {
|
|
||||||
ctx.http
|
|
||||||
.create_guild_application_commands(guild_id.0, &commands_builder)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
ctx.http
|
|
||||||
.create_global_application_commands(&commands_builder)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
if Path::new("/etc/soundfx-rs/default.env").exists() {
|
if Path::new("/etc/soundfx-rs/config.env").exists() {
|
||||||
dotenv::from_path("/etc/soundfx-rs/default.env").unwrap();
|
dotenv::from_path("/etc/soundfx-rs/config.env").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
@ -85,6 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
cmds::manage::download_file(),
|
cmds::manage::download_file(),
|
||||||
cmds::manage::delete_sound(),
|
cmds::manage::delete_sound(),
|
||||||
cmds::play::play(),
|
cmds::play::play(),
|
||||||
|
cmds::play::play_random(),
|
||||||
cmds::play::queue_play(),
|
cmds::play::queue_play(),
|
||||||
cmds::play::loop_play(),
|
cmds::play::loop_play(),
|
||||||
cmds::play::soundboard(),
|
cmds::play::soundboard(),
|
||||||
@ -92,10 +64,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
subcommands: vec![
|
subcommands: vec![
|
||||||
cmds::search::list_guild_sounds(),
|
cmds::search::list_guild_sounds(),
|
||||||
cmds::search::list_user_sounds(),
|
cmds::search::list_user_sounds(),
|
||||||
|
cmds::search::list_favorite_sounds(),
|
||||||
],
|
],
|
||||||
..cmds::search::list_sounds()
|
..cmds::search::list_sounds()
|
||||||
},
|
},
|
||||||
cmds::search::show_random_sounds(),
|
poise::Command {
|
||||||
|
subcommands: vec![
|
||||||
|
cmds::favorite::add_favorite(),
|
||||||
|
cmds::favorite::remove_favorite(),
|
||||||
|
],
|
||||||
|
..cmds::favorite::favorites()
|
||||||
|
},
|
||||||
cmds::search::search_sounds(),
|
cmds::search::search_sounds(),
|
||||||
cmds::stop::stop_playing(),
|
cmds::stop::stop_playing(),
|
||||||
cmds::stop::disconnect(),
|
cmds::stop::disconnect(),
|
||||||
@ -124,7 +103,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
allowed_mentions: None,
|
allowed_mentions: None,
|
||||||
listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),
|
event_handler: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -134,16 +113,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
|
|
||||||
sqlx::migrate!().run(&database).await?;
|
sqlx::migrate!().run(&database).await?;
|
||||||
|
|
||||||
poise::Framework::builder()
|
#[cfg(feature = "metrics")]
|
||||||
.token(discord_token)
|
{
|
||||||
.user_data_setup(move |ctx, _bot, framework| {
|
metrics::init_metrics();
|
||||||
|
tokio::spawn(async { metrics::serve().await });
|
||||||
|
}
|
||||||
|
|
||||||
|
let framework = poise::Framework::builder()
|
||||||
|
.setup(move |ctx, _bot, framework| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
register_application_commands(ctx, framework, None)
|
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(Data {
|
Ok(Data {
|
||||||
http: reqwest::Client::new(),
|
|
||||||
database,
|
database,
|
||||||
guild_data_cache: Default::default(),
|
guild_data_cache: Default::default(),
|
||||||
join_sound_cache: Default::default(),
|
join_sound_cache: Default::default(),
|
||||||
@ -151,10 +132,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.options(options)
|
.options(options)
|
||||||
.client_settings(move |client_builder| client_builder.register_songbird())
|
.build();
|
||||||
.intents(GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS)
|
|
||||||
.run_autosharded()
|
let mut client = ClientBuilder::new(
|
||||||
.await?;
|
&discord_token,
|
||||||
|
GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS,
|
||||||
|
)
|
||||||
|
.activity(ActivityData::watching("for /play"))
|
||||||
|
.framework(framework)
|
||||||
|
.register_songbird()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
client.start_autosharded().await.unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
46
src/metrics.rs
Normal file
46
src/metrics.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use axum::{routing::get, Router};
|
||||||
|
use lazy_static;
|
||||||
|
use log::warn;
|
||||||
|
use prometheus::{register_int_counter, IntCounter, Registry};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref REGISTRY: Registry = Registry::new();
|
||||||
|
pub static ref PLAY_COUNTER: IntCounter =
|
||||||
|
register_int_counter!("play_cmd", "Number of calls to /play").unwrap();
|
||||||
|
pub static ref UPLOAD_COUNTER: IntCounter =
|
||||||
|
register_int_counter!("upload_cmd", "Number of calls to /upload").unwrap();
|
||||||
|
pub static ref DELETE_COUNTER: IntCounter =
|
||||||
|
register_int_counter!("delete_cmd", "Number of calls to /delete").unwrap();
|
||||||
|
pub static ref GREET_COUNTER: IntCounter =
|
||||||
|
register_int_counter!("greet_invoke", "Number of greet sounds played").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_metrics() {
|
||||||
|
REGISTRY.register(Box::new(PLAY_COUNTER.clone())).unwrap();
|
||||||
|
REGISTRY.register(Box::new(UPLOAD_COUNTER.clone())).unwrap();
|
||||||
|
REGISTRY.register(Box::new(DELETE_COUNTER.clone())).unwrap();
|
||||||
|
REGISTRY.register(Box::new(GREET_COUNTER.clone())).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve() {
|
||||||
|
let app = Router::new().route("/metrics", get(metrics));
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("localhost:31755")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn metrics() -> String {
|
||||||
|
let encoder = prometheus::TextEncoder::new();
|
||||||
|
let res_custom = encoder.encode_to_string(®ISTRY.gather());
|
||||||
|
|
||||||
|
match res_custom {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error encoding metrics: {:?}", e);
|
||||||
|
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,12 +78,10 @@ impl GuildData {
|
|||||||
|
|
||||||
let guild_data = sqlx::query_as_unchecked!(
|
let guild_data = sqlx::query_as_unchecked!(
|
||||||
GuildData,
|
GuildData,
|
||||||
"
|
"SELECT id, prefix, volume, allow_greets, allowed_role
|
||||||
SELECT id, prefix, volume, allow_greets, allowed_role
|
FROM servers
|
||||||
FROM servers
|
WHERE id = ?",
|
||||||
WHERE id = ?
|
guild_id.get()
|
||||||
",
|
|
||||||
guild_id.as_u64()
|
|
||||||
)
|
)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
.await;
|
.await;
|
||||||
@ -104,17 +102,15 @@ SELECT id, prefix, volume, allow_greets, allowed_role
|
|||||||
let guild_id = guild_id.into();
|
let guild_id = guild_id.into();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"INSERT INTO servers (id)
|
||||||
INSERT INTO servers (id)
|
VALUES (?)",
|
||||||
VALUES (?)
|
guild_id.get()
|
||||||
",
|
|
||||||
guild_id.as_u64()
|
|
||||||
)
|
)
|
||||||
.execute(db_pool)
|
.execute(db_pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(GuildData {
|
Ok(GuildData {
|
||||||
id: guild_id.as_u64().to_owned(),
|
id: guild_id.get(),
|
||||||
prefix: String::from("?"),
|
prefix: String::from("?"),
|
||||||
volume: 100,
|
volume: 100,
|
||||||
allow_greets: AllowGreet::Enabled,
|
allow_greets: AllowGreet::Enabled,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use poise::serenity_prelude::{async_trait, model::id::UserId, GuildId};
|
use poise::serenity_prelude::{async_trait, model::id::UserId, GuildId};
|
||||||
|
use sqlx::Acquire;
|
||||||
|
|
||||||
use crate::Data;
|
use crate::Data;
|
||||||
|
|
||||||
@ -47,14 +48,13 @@ impl JoinSoundCtx for Data {
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
JoinSound,
|
JoinSound,
|
||||||
"
|
"
|
||||||
SELECT join_sound_id
|
SELECT join_sound_id
|
||||||
FROM join_sounds
|
FROM join_sounds
|
||||||
WHERE user = ?
|
WHERE user = ?
|
||||||
AND guild = ?
|
AND guild = ?
|
||||||
ORDER BY guild IS NULL
|
ORDER BY guild IS NULL",
|
||||||
",
|
user_id.get(),
|
||||||
user_id.as_u64(),
|
guild_id.map(|g| g.get())
|
||||||
guild_id.map(|g| g.0)
|
|
||||||
)
|
)
|
||||||
.fetch_one(&self.database)
|
.fetch_one(&self.database)
|
||||||
.await
|
.await
|
||||||
@ -62,14 +62,13 @@ SELECT join_sound_id
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
JoinSound,
|
JoinSound,
|
||||||
"
|
"
|
||||||
SELECT join_sound_id
|
SELECT join_sound_id
|
||||||
FROM join_sounds
|
FROM join_sounds
|
||||||
WHERE user = ?
|
WHERE user = ?
|
||||||
AND (guild IS NULL OR guild = ?)
|
AND (guild IS NULL OR guild = ?)
|
||||||
ORDER BY guild IS NULL
|
ORDER BY guild IS NULL",
|
||||||
",
|
user_id.get(),
|
||||||
user_id.as_u64(),
|
guild_id.map(|g| g.get())
|
||||||
guild_id.map(|g| g.0)
|
|
||||||
)
|
)
|
||||||
.fetch_one(&self.database)
|
.fetch_one(&self.database)
|
||||||
.await
|
.await
|
||||||
@ -111,29 +110,29 @@ SELECT join_sound_id
|
|||||||
Some(join_id) => {
|
Some(join_id) => {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
||||||
user_id.0,
|
user_id.get(),
|
||||||
guild_id.map(|g| g.0)
|
guild_id.map(|g| g.get())
|
||||||
)
|
)
|
||||||
.execute(&mut transaction)
|
.execute(transaction.acquire().await?)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO join_sounds (user, join_sound_id, guild) VALUES (?, ?, ?)",
|
"INSERT INTO join_sounds (user, join_sound_id, guild) VALUES (?, ?, ?)",
|
||||||
user_id.0,
|
user_id.get(),
|
||||||
join_id,
|
join_id,
|
||||||
guild_id.map(|g| g.0)
|
guild_id.map(|g| g.get())
|
||||||
)
|
)
|
||||||
.execute(&mut transaction)
|
.execute(transaction.acquire().await?)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
"DELETE FROM join_sounds WHERE user = ? AND guild <=> ?",
|
||||||
user_id.0,
|
user_id.get(),
|
||||||
guild_id.map(|g| g.0)
|
guild_id.map(|g| g.get())
|
||||||
)
|
)
|
||||||
.execute(&mut transaction)
|
.execute(transaction.acquire().await?)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
use std::{env, path::Path};
|
|
||||||
|
|
||||||
use poise::serenity_prelude::async_trait;
|
use poise::serenity_prelude::async_trait;
|
||||||
use songbird::input::restartable::Restartable;
|
use songbird::input::Input;
|
||||||
use sqlx::Executor;
|
use sqlx::Executor;
|
||||||
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database};
|
use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database};
|
||||||
|
|
||||||
@ -37,17 +35,31 @@ pub trait SoundCtx {
|
|||||||
user_id: U,
|
user_id: U,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
|
async fn autocomplete_favorite_sounds<U: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
user_id: U,
|
||||||
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
async fn user_sounds<U: Into<u64> + Send>(
|
async fn user_sounds<U: Into<u64> + Send>(
|
||||||
&self,
|
&self,
|
||||||
user_id: U,
|
user_id: U,
|
||||||
page: Option<u64>,
|
page: Option<u64>,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
|
async fn favorite_sounds<U: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
page: Option<u64>,
|
||||||
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
async fn guild_sounds<G: Into<u64> + Send>(
|
async fn guild_sounds<G: Into<u64> + Send>(
|
||||||
&self,
|
&self,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
page: Option<u64>,
|
page: Option<u64>,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error>;
|
) -> Result<Vec<Sound>, sqlx::Error>;
|
||||||
async fn count_user_sounds<U: Into<u64> + Send>(&self, user_id: U) -> Result<u64, sqlx::Error>;
|
async fn count_user_sounds<U: Into<u64> + Send>(&self, user_id: U) -> Result<u64, sqlx::Error>;
|
||||||
|
async fn count_favorite_sounds<U: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
) -> Result<u64, sqlx::Error>;
|
||||||
async fn count_guild_sounds<G: Into<u64> + Send>(
|
async fn count_guild_sounds<G: Into<u64> + Send>(
|
||||||
&self,
|
&self,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
@ -85,14 +97,13 @@ impl SoundCtx for Data {
|
|||||||
let sound = sqlx::query_as_unchecked!(
|
let sound = sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE id = ? AND (
|
WHERE id = ? AND (
|
||||||
public = 1 OR
|
public = 1 OR
|
||||||
uploader_id = ? OR
|
uploader_id = ? OR
|
||||||
server_id = ?
|
server_id = ?
|
||||||
)
|
)",
|
||||||
",
|
|
||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
guild_id
|
guild_id
|
||||||
@ -109,19 +120,28 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
sound = sqlx::query_as_unchecked!(
|
sound = sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE name = ? AND (
|
WHERE name = ? AND (
|
||||||
public = 1 OR
|
public = 1 OR
|
||||||
uploader_id = ? OR
|
uploader_id = ? OR
|
||||||
server_id = ?
|
server_id = ?
|
||||||
)
|
)
|
||||||
ORDER BY uploader_id = ? DESC, server_id = ? DESC, public = 1 DESC, rand()
|
ORDER BY
|
||||||
",
|
uploader_id = ? DESC,
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM favorite_sounds
|
||||||
|
WHERE sound_id = id AND user_id = ?
|
||||||
|
) DESC,
|
||||||
|
server_id = ? DESC,
|
||||||
|
public = 1 DESC,
|
||||||
|
rand()",
|
||||||
name,
|
name,
|
||||||
user_id,
|
user_id,
|
||||||
guild_id,
|
guild_id,
|
||||||
user_id,
|
user_id,
|
||||||
|
user_id,
|
||||||
guild_id
|
guild_id
|
||||||
)
|
)
|
||||||
.fetch_all(&db_pool)
|
.fetch_all(&db_pool)
|
||||||
@ -130,19 +150,28 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
sound = sqlx::query_as_unchecked!(
|
sound = sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE name LIKE CONCAT('%', ?, '%') AND (
|
WHERE name LIKE CONCAT('%', ?, '%') AND (
|
||||||
public = 1 OR
|
public = 1 OR
|
||||||
uploader_id = ? OR
|
uploader_id = ? OR
|
||||||
server_id = ?
|
server_id = ?
|
||||||
)
|
)
|
||||||
ORDER BY uploader_id = ? DESC, server_id = ? DESC, public = 1 DESC, rand()
|
ORDER BY
|
||||||
",
|
uploader_id = ? DESC,
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM favorite_sounds
|
||||||
|
WHERE sound_id = id AND user_id = ?
|
||||||
|
) DESC,
|
||||||
|
server_id = ? DESC,
|
||||||
|
public = 1 DESC,
|
||||||
|
rand()",
|
||||||
name,
|
name,
|
||||||
user_id,
|
user_id,
|
||||||
guild_id,
|
guild_id,
|
||||||
user_id,
|
user_id,
|
||||||
|
user_id,
|
||||||
guild_id
|
guild_id
|
||||||
)
|
)
|
||||||
.fetch_all(&db_pool)
|
.fetch_all(&db_pool)
|
||||||
@ -160,18 +189,48 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
guild_id: G,
|
guild_id: G,
|
||||||
) -> Result<Vec<Sound>, sqlx::Error> {
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
let db_pool = self.database.clone();
|
let db_pool = self.database.clone();
|
||||||
|
let user_id = user_id.into();
|
||||||
|
|
||||||
sqlx::query_as_unchecked!(
|
sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE name LIKE CONCAT(?, '%') AND (uploader_id = ? OR server_id = ?)
|
WHERE name LIKE CONCAT(?, '%') AND (uploader_id = ? OR server_id = ? OR EXISTS(
|
||||||
LIMIT 25
|
SELECT 1
|
||||||
",
|
FROM favorite_sounds
|
||||||
|
WHERE sound_id = id AND user_id = ?
|
||||||
|
))
|
||||||
|
LIMIT 25",
|
||||||
|
query,
|
||||||
|
user_id,
|
||||||
|
guild_id.into(),
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
.fetch_all(&db_pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn autocomplete_favorite_sounds<U: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
user_id: U,
|
||||||
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
|
let db_pool = self.database.clone();
|
||||||
|
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
|
Sound,
|
||||||
|
"
|
||||||
|
SELECT name, id, public, server_id, uploader_id
|
||||||
|
FROM sounds
|
||||||
|
WHERE name LIKE CONCAT(?, '%') AND EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM favorite_sounds
|
||||||
|
WHERE sound_id = id AND user_id = ?
|
||||||
|
)
|
||||||
|
LIMIT 25",
|
||||||
query,
|
query,
|
||||||
user_id.into(),
|
user_id.into(),
|
||||||
guild_id.into(),
|
|
||||||
)
|
)
|
||||||
.fetch_all(&db_pool)
|
.fetch_all(&db_pool)
|
||||||
.await
|
.await
|
||||||
@ -187,12 +246,11 @@ LIMIT 25
|
|||||||
sqlx::query_as_unchecked!(
|
sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE uploader_id = ?
|
WHERE uploader_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?, ?
|
LIMIT ?, ?",
|
||||||
",
|
|
||||||
user_id.into(),
|
user_id.into(),
|
||||||
page * 25,
|
page * 25,
|
||||||
(page + 1) * 25
|
(page + 1) * 25
|
||||||
@ -204,11 +262,52 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
sqlx::query_as_unchecked!(
|
sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE uploader_id = ?
|
WHERE uploader_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC",
|
||||||
",
|
user_id.into()
|
||||||
|
)
|
||||||
|
.fetch_all(&self.database)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(sounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn favorite_sounds<U: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
page: Option<u64>,
|
||||||
|
) -> Result<Vec<Sound>, sqlx::Error> {
|
||||||
|
let sounds = match page {
|
||||||
|
Some(page) => {
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
|
Sound,
|
||||||
|
"
|
||||||
|
SELECT name, id, public, server_id, uploader_id
|
||||||
|
FROM sounds
|
||||||
|
INNER JOIN favorite_sounds f ON sounds.id = f.sound_id
|
||||||
|
WHERE f.user_id = ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?, ?",
|
||||||
|
user_id.into(),
|
||||||
|
page * 25,
|
||||||
|
(page + 1) * 25
|
||||||
|
)
|
||||||
|
.fetch_all(&self.database)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
|
Sound,
|
||||||
|
"
|
||||||
|
SELECT name, id, public, server_id, uploader_id
|
||||||
|
FROM sounds
|
||||||
|
INNER JOIN favorite_sounds f ON sounds.id = f.sound_id
|
||||||
|
WHERE f.user_id = ?
|
||||||
|
ORDER BY id DESC",
|
||||||
user_id.into()
|
user_id.into()
|
||||||
)
|
)
|
||||||
.fetch_all(&self.database)
|
.fetch_all(&self.database)
|
||||||
@ -229,12 +328,11 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
sqlx::query_as_unchecked!(
|
sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE server_id = ?
|
WHERE server_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?, ?
|
LIMIT ?, ?",
|
||||||
",
|
|
||||||
guild_id.into(),
|
guild_id.into(),
|
||||||
page * 25,
|
page * 25,
|
||||||
(page + 1) * 25
|
(page + 1) * 25
|
||||||
@ -247,11 +345,10 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
sqlx::query_as_unchecked!(
|
sqlx::query_as_unchecked!(
|
||||||
Sound,
|
Sound,
|
||||||
"
|
"
|
||||||
SELECT name, id, public, server_id, uploader_id
|
SELECT name, id, public, server_id, uploader_id
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE server_id = ?
|
WHERE server_id = ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC",
|
||||||
",
|
|
||||||
guild_id.into()
|
guild_id.into()
|
||||||
)
|
)
|
||||||
.fetch_all(&self.database)
|
.fetch_all(&self.database)
|
||||||
@ -272,6 +369,19 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
.count as u64)
|
.count as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn count_favorite_sounds<U: Into<u64> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
) -> Result<u64, sqlx::Error> {
|
||||||
|
Ok(sqlx::query!(
|
||||||
|
"SELECT COUNT(1) as count FROM favorite_sounds WHERE user_id = ?",
|
||||||
|
user_id.into()
|
||||||
|
)
|
||||||
|
.fetch_one(&self.database)
|
||||||
|
.await?
|
||||||
|
.count as u64)
|
||||||
|
}
|
||||||
|
|
||||||
async fn count_guild_sounds<G: Into<u64> + Send>(
|
async fn count_guild_sounds<G: Into<u64> + Send>(
|
||||||
&self,
|
&self,
|
||||||
guild_id: G,
|
guild_id: G,
|
||||||
@ -287,7 +397,7 @@ SELECT name, id, public, server_id, uploader_id
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Sound {
|
impl Sound {
|
||||||
async fn src(&self, db_pool: impl Executor<'_, Database = Database>) -> Vec<u8> {
|
pub(crate) async fn src(&self, db_pool: impl Executor<'_, Database = Database>) -> Vec<u8> {
|
||||||
struct Src {
|
struct Src {
|
||||||
src: Vec<u8>,
|
src: Vec<u8>,
|
||||||
}
|
}
|
||||||
@ -295,11 +405,10 @@ impl Sound {
|
|||||||
let record = sqlx::query_as_unchecked!(
|
let record = sqlx::query_as_unchecked!(
|
||||||
Src,
|
Src,
|
||||||
"
|
"
|
||||||
SELECT src
|
SELECT src
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
LIMIT 1
|
LIMIT 1",
|
||||||
",
|
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
@ -309,33 +418,11 @@ SELECT src
|
|||||||
record.src
|
record.src
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn store_sound_source(
|
|
||||||
&self,
|
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
|
||||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
let caching_location = env::var("CACHING_LOCATION").unwrap_or(String::from("/tmp"));
|
|
||||||
|
|
||||||
let path_name = format!("{}/sound-{}", caching_location, self.id);
|
|
||||||
let path = Path::new(&path_name);
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
let mut file = File::create(&path).await?;
|
|
||||||
|
|
||||||
file.write_all(&self.src(db_pool).await).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(path_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn playable(
|
pub async fn playable(
|
||||||
&self,
|
&self,
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Input, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let path_name = self.store_sound_source(db_pool).await?;
|
Ok(Input::from(self.src(db_pool).await))
|
||||||
|
|
||||||
Ok(Restartable::ffmpeg(path_name, false)
|
|
||||||
.await
|
|
||||||
.expect("FFMPEG ERROR!"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn count_user_sounds<U: Into<u64>>(
|
pub async fn count_user_sounds<U: Into<u64>>(
|
||||||
@ -346,10 +433,9 @@ SELECT src
|
|||||||
|
|
||||||
let c = sqlx::query!(
|
let c = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT COUNT(1) as count
|
SELECT COUNT(1) as count
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE uploader_id = ?
|
WHERE uploader_id = ?",
|
||||||
",
|
|
||||||
user_id
|
user_id
|
||||||
)
|
)
|
||||||
.fetch_one(db_pool)
|
.fetch_one(db_pool)
|
||||||
@ -368,12 +454,11 @@ SELECT COUNT(1) as count
|
|||||||
|
|
||||||
let c = sqlx::query!(
|
let c = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT COUNT(1) as count
|
SELECT COUNT(1) as count
|
||||||
FROM sounds
|
FROM sounds
|
||||||
WHERE
|
WHERE
|
||||||
uploader_id = ? AND
|
uploader_id = ? AND
|
||||||
name = ?
|
name = ?",
|
||||||
",
|
|
||||||
user_id,
|
user_id,
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
@ -390,12 +475,11 @@ SELECT COUNT(1) as count
|
|||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE sounds
|
UPDATE sounds
|
||||||
SET
|
SET
|
||||||
public = ?
|
public = ?
|
||||||
WHERE
|
WHERE
|
||||||
id = ?
|
id = ?",
|
||||||
",
|
|
||||||
self.public,
|
self.public,
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
@ -409,12 +493,41 @@ WHERE
|
|||||||
&self,
|
&self,
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
sqlx::query!("DELETE FROM sounds WHERE id = ?", self.id)
|
||||||
|
.execute(db_pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_favorite<U: Into<u64>>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync + Send>> {
|
||||||
|
let user_id = user_id.into();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"INSERT INTO favorite_sounds (user_id, sound_id) VALUES (?, ?)",
|
||||||
DELETE
|
user_id,
|
||||||
FROM sounds
|
self.id
|
||||||
WHERE id = ?
|
)
|
||||||
",
|
.execute(db_pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_favorite<U: Into<u64>>(
|
||||||
|
&self,
|
||||||
|
user_id: U,
|
||||||
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync + Send>> {
|
||||||
|
let user_id = user_id.into();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM favorite_sounds WHERE user_id = ? AND sound_id = ?",
|
||||||
|
user_id,
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
.execute(db_pool)
|
.execute(db_pool)
|
||||||
@ -467,9 +580,8 @@ DELETE
|
|||||||
Some(data) => {
|
Some(data) => {
|
||||||
match sqlx::query!(
|
match sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO sounds (name, server_id, uploader_id, public, src)
|
INSERT INTO sounds (name, server_id, uploader_id, public, src)
|
||||||
VALUES (?, ?, ?, 1, ?)
|
VALUES (?, ?, ?, 1, ?)",
|
||||||
",
|
|
||||||
name,
|
name,
|
||||||
server_id,
|
server_id,
|
||||||
user_id,
|
user_id,
|
||||||
|
102
src/utils.rs
102
src/utils.rs
@ -1,11 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
use poise::serenity_prelude::model::{
|
use poise::serenity_prelude::{
|
||||||
channel::Channel,
|
model::{
|
||||||
guild::Guild,
|
guild::Guild,
|
||||||
id::{ChannelId, UserId},
|
id::{ChannelId, UserId},
|
||||||
|
},
|
||||||
|
ChannelType, EditVoiceState, GuildId,
|
||||||
};
|
};
|
||||||
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call};
|
use songbird::{tracks::TrackHandle, Call};
|
||||||
use sqlx::Executor;
|
use sqlx::Executor;
|
||||||
use tokio::sync::{Mutex, MutexGuard};
|
use tokio::sync::{Mutex, MutexGuard};
|
||||||
|
|
||||||
@ -22,21 +24,20 @@ pub async fn play_audio(
|
|||||||
volume: u8,
|
volume: u8,
|
||||||
call_handler: &mut MutexGuard<'_, Call>,
|
call_handler: &mut MutexGuard<'_, Call>,
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
db_pool: impl Executor<'_, Database = Database>,
|
||||||
loop_: bool,
|
r#loop: bool,
|
||||||
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let (track, track_handler) = create_player(sound.playable(db_pool).await?.into());
|
let track = sound.playable(db_pool).await?;
|
||||||
|
let handle = call_handler.play_input(track);
|
||||||
|
|
||||||
let _ = track_handler.set_volume(volume as f32 / 100.0);
|
handle.set_volume(volume as f32 / 100.0)?;
|
||||||
|
|
||||||
if loop_ {
|
if r#loop {
|
||||||
let _ = track_handler.enable_loop();
|
handle.enable_loop()?;
|
||||||
} else {
|
} else {
|
||||||
let _ = track_handler.disable_loop();
|
handle.disable_loop()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
call_handler.play(track);
|
Ok(handle)
|
||||||
|
|
||||||
Ok(track_handler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn queue_audio(
|
pub async fn queue_audio(
|
||||||
@ -46,11 +47,10 @@ pub async fn queue_audio(
|
|||||||
db_pool: impl Executor<'_, Database = Database> + Copy,
|
db_pool: impl Executor<'_, Database = Database> + Copy,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
for sound in sounds {
|
for sound in sounds {
|
||||||
let (a, b) = create_player(sound.playable(db_pool).await?.into());
|
let track = sound.playable(db_pool).await?;
|
||||||
|
let handle = call_handler.enqueue_input(track).await;
|
||||||
|
|
||||||
let _ = b.set_volume(volume as f32 / 100.0);
|
handle.set_volume(volume as f32 / 100.0)?;
|
||||||
|
|
||||||
call_handler.enqueue(a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -58,60 +58,65 @@ pub async fn queue_audio(
|
|||||||
|
|
||||||
pub async fn join_channel(
|
pub async fn join_channel(
|
||||||
ctx: &poise::serenity_prelude::Context,
|
ctx: &poise::serenity_prelude::Context,
|
||||||
guild: Guild,
|
guild_id: GuildId,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
) -> (Arc<Mutex<Call>>, JoinResult<()>) {
|
) -> Result<Arc<Mutex<Call>>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let songbird = songbird::get(ctx).await.unwrap();
|
let songbird = songbird::get(ctx).await.unwrap();
|
||||||
let current_user = ctx.cache.current_user_id();
|
let current_user = ctx.cache.current_user().id;
|
||||||
|
|
||||||
let current_voice_state = guild
|
let current_voice_state = ctx
|
||||||
.voice_states
|
.cache
|
||||||
.get(¤t_user)
|
.guild(guild_id)
|
||||||
.and_then(|voice_state| voice_state.channel_id);
|
.map(|g| {
|
||||||
|
g.voice_states
|
||||||
|
.get(¤t_user)
|
||||||
|
.and_then(|voice_state| voice_state.channel_id)
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let (call, res) = if current_voice_state == Some(channel_id) {
|
let call = if current_voice_state == Some(channel_id) {
|
||||||
let call_opt = songbird.get(guild.id);
|
let call_opt = songbird.get(guild_id);
|
||||||
|
|
||||||
if let Some(call) = call_opt {
|
if let Some(call) = call_opt {
|
||||||
(call, Ok(()))
|
Ok(call)
|
||||||
} else {
|
} else {
|
||||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
songbird.join(guild_id, channel_id).await
|
||||||
|
|
||||||
(call, res)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let (call, res) = songbird.join(guild.id, channel_id).await;
|
songbird.join(guild_id, channel_id).await
|
||||||
|
}?;
|
||||||
(call, res)
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
{
|
||||||
// set call to deafen
|
call.lock().await.deafen(true).await?;
|
||||||
let _ = call.lock().await.deafen(true).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
if let Some(channel) = ctx.cache.channel(channel_id).map(|c| c.clone()) {
|
||||||
let _ = channel
|
if channel.kind == ChannelType::Stage {
|
||||||
.edit_voice_state(&ctx, ctx.cache.current_user(), |v| v.suppress(false))
|
let user_id = ctx.cache.current_user().id.clone();
|
||||||
.await;
|
|
||||||
|
channel
|
||||||
|
.edit_voice_state(&ctx, user_id, EditVoiceState::new().suppress(true))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(call, res)
|
Ok(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play_from_query(
|
pub async fn play_from_query(
|
||||||
ctx: &poise::serenity_prelude::Context,
|
ctx: &poise::serenity_prelude::Context,
|
||||||
data: &Data,
|
data: &Data,
|
||||||
guild: Guild,
|
guild: impl Deref<Target = Guild> + Send + Sync,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
channel: Option<ChannelId>,
|
channel: Option<ChannelId>,
|
||||||
query: &str,
|
query: &str,
|
||||||
loop_: bool,
|
r#loop: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let guild_id = guild.id;
|
let guild_id = guild.deref().id;
|
||||||
|
|
||||||
let channel_to_join = channel.or_else(|| {
|
let channel_to_join = channel.or_else(|| {
|
||||||
guild
|
guild
|
||||||
|
.deref()
|
||||||
.voice_states
|
.voice_states
|
||||||
.get(&user_id)
|
.get(&user_id)
|
||||||
.and_then(|voice_state| voice_state.channel_id)
|
.and_then(|voice_state| voice_state.channel_id)
|
||||||
@ -129,8 +134,7 @@ pub async fn play_from_query(
|
|||||||
match sound_res {
|
match sound_res {
|
||||||
Some(sound) => {
|
Some(sound) => {
|
||||||
{
|
{
|
||||||
let (call_handler, _) =
|
let call_handler = join_channel(ctx, guild_id, user_channel).await.unwrap();
|
||||||
join_channel(ctx, guild.clone(), user_channel).await;
|
|
||||||
|
|
||||||
let guild_data = data.guild_data(guild_id).await.unwrap();
|
let guild_data = data.guild_data(guild_id).await.unwrap();
|
||||||
|
|
||||||
@ -141,7 +145,7 @@ pub async fn play_from_query(
|
|||||||
guild_data.read().await.volume,
|
guild_data.read().await.volume,
|
||||||
&mut lock,
|
&mut lock,
|
||||||
&data.database,
|
&data.database,
|
||||||
loop_,
|
r#loop,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
Description=Discord bot for custom sound effects and soundboards
|
Description=Discord bot for custom sound effects and soundboards
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
User=soundfx
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/bin/soundfx-rs
|
ExecStart=/usr/bin/soundfx-rs
|
||||||
WorkingDirectory=/etc/soundfx-rs
|
WorkingDirectory=/etc/soundfx-rs
|
||||||
|
Loading…
Reference in New Issue
Block a user