Compare commits

..

No commits in common. "rewrite" and "v1.5.8" have entirely different histories.

35 changed files with 1625 additions and 3410 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
/target /target
.env .env
.idea

2
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Default ignored files
/workspace.xml

View File

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

11
.idea/dataSources.xml Normal file
View File

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

View File

@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="jude">
<words>
<w>reqwest</w>
</words>
</dictionary>
</component>

View File

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

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

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

14
.idea/soundfx-rs.iml Normal file
View File

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

6
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3294
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,45 +2,35 @@
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.18" version = "1.5.8"
authors = ["jellywx <judesouthworth@pm.me>"] authors = ["jellywx <judesouthworth@pm.me>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
songbird = { version = "0.4", features = ["builtin-queue"] } songbird = { version = "0.3", features = ["builtin-queue"] }
poise = "0.6.1-rc1" poise = "0.3"
sqlx = { version = "0.7.3", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] } sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "migrate"] }
tokio = { version = "1", features = ["fs", "process", "io-util", "rt-multi-thread"] } tokio = { version = "1", features = ["fs", "process", "io-util"] }
lazy_static = "1.4" lazy_static = "1.4"
reqwest = "0.12" reqwest = "0.11"
env_logger = "0.11" env_logger = "0.10"
regex = "1.10" regex = "1.4"
log = "0.4" log = "0.4"
serde_json = "1.0" serde_json = "1.0"
dashmap = "6.0" dashmap = "5.3"
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 }
[dependencies.symphonia] [patch."https://github.com/serenity-rs/serenity"]
version = "0.5" serenity = { version = "0.11.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/config.env", "600"] ["conf/default.env", "etc/soundfx-rs/default.env", "600"]
]
conf-files = [
"/etc/soundfx-rs/config.env",
] ]
[package.metadata.deb.systemd-units] [package.metadata.deb.systemd-units]

View File

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

View File

@ -23,26 +23,11 @@ Options:
## Building from source ## Building from source
When running from source, the config options above can be configured simply as environment variables. 1. Install build dependencies: `sudo apt install gcc gcc-multilib cmake ffmpeg libopus-dev`
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`
### Build for other platform When running from source, the config options above can be configured simply as environment variables.
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`

2
debian/.gitignore vendored
View File

@ -1,2 +0,0 @@
*
!.gitignore

4
debian/postinst vendored
View File

@ -4,6 +4,10 @@ set -e
id -u soundfx &>/dev/null || useradd -r -M soundfx id -u soundfx &>/dev/null || useradd -r -M soundfx
if [ ! -f /etc/soundfx-rs/config.env ]; then
cp /etc/soundfx-rs/default.env /etc/soundfx-rs/config.env
fi
chown soundfx /etc/soundfx-rs/config.env chown soundfx /etc/soundfx-rs/config.env
#DEBHELPER# #DEBHELPER#

4
debian/postrm vendored
View File

@ -4,4 +4,8 @@ set -e
id -u soundfx &>/dev/null || userdel soundfx id -u soundfx &>/dev/null || userdel soundfx
if [ -f /etc/soundfx-rs/config.env ]; then
rm /etc/soundfx/config.env
fi
#DEBHELPER# #DEBHELPER#

View File

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

View File

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

View File

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

View File

@ -1,23 +1,19 @@
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( ctx.send(|m| {
CreateReply::default().ephemeral(true).embed( m.ephemeral(true).embed(|e| {
CreateEmbed::new() e.title("Help")
.title("Help")
.color(THEME_COLOR) .color(THEME_COLOR)
.footer(CreateEmbedFooter::new(concat!( .footer(|f| {
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`
@ -37,9 +33,6 @@ __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
@ -53,9 +46,9 @@ __Setting Commands__
__Advanced Commands__ __Advanced Commands__
`/soundboard` - Create a soundboard", `/soundboard` - Create a soundboard",
),
),
) )
})
})
.await?; .await?;
Ok(()) Ok(())
@ -64,18 +57,14 @@ __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.serenity_context().cache.current_user().id.get(); let current_user = ctx.discord().cache.current_user();
ctx.send( ctx.send(|m| m.ephemeral(true)
CreateReply::default().ephemeral(true).embed( .embed(|e| e
CreateEmbed::new()
.title("Info") .title("Info")
.color(THEME_COLOR) .color(THEME_COLOR)
.footer(CreateEmbedFooter::new(concat!( .footer(|f| f
env!("CARGO_PKG_NAME"), .text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION"))))
" ver ",
env!("CARGO_PKG_VERSION")
)))
.description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot .description(format!("Invite me: https://discord.com/api/oauth2/authorize?client_id={}&permissions=3165184&scope=applications.commands%20bot
**Welcome to SoundFX!** **Welcome to SoundFX!**
@ -84,9 +73,7 @@ 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))) current_user.id.as_u64())))).await?;
)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,10 +1,6 @@
use poise::{ use poise::serenity_prelude::{Attachment, GuildId, RoleId};
serenity_prelude::{Attachment, CreateAttachment, GuildId, RoleId}, use tokio::fs::File;
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},
@ -25,9 +21,6 @@ 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 {
@ -42,13 +35,7 @@ pub async fn upload_new_sound(
} }
if !name.is_empty() && name.len() <= 20 { if !name.is_empty() && name.len() <= 20 {
if name.starts_with("@") { if !is_numeric(&name) {
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)
@ -63,14 +50,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 not // need to check if user is patreon or nah
if count >= *MAX_SOUNDS { if count >= *MAX_SOUNDS {
let patreon_guild_member = GuildId::from(*PATREON_GUILD) let patreon_guild_member = GuildId(*PATREON_GUILD)
.member(ctx, ctx.author().id) .member(ctx.discord(), 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::from(*PATREON_ROLE)); permit_upload = member.roles.contains(&RoleId(*PATREON_ROLE));
} else { } else {
permit_upload = false; permit_upload = false;
} }
@ -102,6 +89,9 @@ 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?;
@ -118,13 +108,10 @@ 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.get(); let uid = ctx.author().id.0;
let gid = ctx.guild_id().unwrap().get(); let gid = ctx.guild_id().unwrap().0;
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();
@ -136,8 +123,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, uid).await { if let Ok(member) = ctx.guild_id().unwrap().member(&ctx.discord(), uid).await {
if let Ok(perms) = member.permissions(&ctx) { if let Ok(perms) = member.permissions(&ctx.discord()) {
perms.manage_guild() perms.manage_guild()
} else { } else {
false false
@ -176,8 +163,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.get(); let uid = ctx.author().id.0;
let gid = ctx.guild_id().unwrap().get(); let gid = ctx.guild_id().unwrap().0;
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();
@ -226,12 +213,12 @@ 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(CreateReply::default().attachment(CreateAttachment::bytes( ctx.send(|m| m.attachment((&file, name.as_str()).into()))
sound.src(&ctx.data().database).await,
name.as_str(),
)))
.await?; .await?;
} }

View File

@ -1,8 +1,5 @@
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;
@ -10,22 +7,18 @@ pub mod search;
pub mod settings; pub mod settings;
pub mod stop; pub mod stop;
pub async fn autocomplete_sound(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> { pub async fn autocomplete_sound(
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| AutocompleteChoice::new(s.name.clone(), s.id.to_string())) .map(|s| poise::AutocompleteChoice {
.collect() name: s.name.clone(),
} 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()
} }

View File

@ -1,18 +1,11 @@
use std::time::{SystemTime, UNIX_EPOCH}; use poise::serenity_prelude::{
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_audio, play_from_query, queue_audio}, utils::{join_channel, play_from_query, queue_audio},
Context, Error, Context, Error,
}; };
@ -27,18 +20,19 @@ 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().map(|g| g.clone()).unwrap(); let guild = ctx.guild().unwrap();
if channel.as_ref().map_or(false, |c| c.is_text_based()) {
ctx.say("The channel specified is not a voice channel.")
.await?;
} else {
ctx.say( ctx.say(
play_from_query( play_from_query(
&ctx.serenity_context(), &ctx.discord(),
&ctx.data(), &ctx.data(),
&guild, guild,
ctx.author().id, ctx.author().id,
channel.map(|c| c.id), channel.map(|c| c.id),
&name, &name,
@ -47,83 +41,6 @@ pub async fn play(
.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(())
@ -216,23 +133,24 @@ pub async fn queue_play(
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.defer().await?; ctx.defer().await?;
let (channel_to_join, guild_id) = {
let guild = ctx.guild().unwrap(); let guild = ctx.guild().unwrap();
( let channel_to_join = guild
guild
.voice_states .voice_states
.get(&ctx.author().id) .get(&ctx.author().id)
.and_then(|voice_state| voice_state.channel_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 = join_channel(ctx.serenity_context(), guild_id, user_channel).await?; let (call_handler, _) = join_channel(ctx.discord(), guild.clone(), user_channel).await;
let guild_data = ctx.data().guild_data(guild_id).await.unwrap(); let guild_data = ctx
.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),
@ -275,9 +193,6 @@ pub async fn queue_play(
} }
} }
{
let mut lock = call.lock().await;
queue_audio( queue_audio(
&sounds, &sounds,
guild_data.read().await.volume, guild_data.read().await.volume,
@ -286,7 +201,6 @@ pub async fn queue_play(
) )
.await .await
.unwrap(); .unwrap();
}
ctx.say(format!("Queued {} sounds!", sounds.len())).await?; ctx.say(format!("Queued {} sounds!", sounds.len())).await?;
} }
@ -313,13 +227,13 @@ pub async fn loop_play(
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.defer().await?; ctx.defer().await?;
let guild = ctx.guild().map(|g| g.clone()).unwrap(); let guild = ctx.guild().unwrap();
ctx.say( ctx.say(
play_from_query( play_from_query(
&ctx.serenity_context(), &ctx.discord(),
&ctx.data(), &ctx.data(),
&guild, guild,
ctx.author().id, ctx.author().id,
None, None,
&name, &name,
@ -402,6 +316,21 @@ 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?;
@ -426,6 +355,11 @@ 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![];
@ -443,49 +377,24 @@ pub async fn soundboard(
} }
} }
let components = { ctx.send(|m| {
let mut c = vec![]; m.content("**Play a sound:**").components(|c| {
for row in sounds.as_slice().chunks(5) { for row in sounds.as_slice().chunks(5) {
let mut action_row = vec![]; let mut action_row: CreateActionRow = Default::default();
for sound in row { for sound in row {
action_row.push( action_row.create_button(|b| {
CreateButton::new(sound.id.to_string()) b.style(ButtonStyle::Primary)
.style(ButtonStyle::Primary) .label(&sound.name)
.label(&sound.name), .custom_id(sound.id)
); });
} }
c.push(CreateActionRow::Buttons(action_row)); c.add_action_row(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 c
}; })
})
ctx.send(
CreateReply::default()
.content("**Play a sound:**")
.components(components),
)
.await?; .await?;
Ok(()) Ok(())

View File

@ -1,8 +1,10 @@
use poise::{ use poise::{
serenity_prelude, serenity_prelude,
serenity_prelude::{ serenity_prelude::{
constants::MESSAGE_CODE_LIMIT, ButtonStyle, ComponentInteraction, CreateActionRow, application::component::ButtonStyle,
CreateButton, CreateEmbed, EditInteractionResponse, GuildId, UserId, constants::MESSAGE_CODE_LIMIT,
interaction::{message_component::MessageComponentInteraction, InteractionResponseType},
CreateActionRow, CreateEmbed, GuildId, UserId,
}, },
CreateReply, CreateReply,
}; };
@ -14,8 +16,8 @@ use crate::{
Context, Data, Error, Context, Data, Error,
}; };
fn format_search_results(search_results: Vec<Sound>) -> CreateReply { fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
let builder = CreateReply::default(); let mut 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:";
@ -30,7 +32,9 @@ fn format_search_results(search_results: Vec<Sound>) -> CreateReply {
current_character_count <= MESSAGE_CODE_LIMIT - title.len() current_character_count <= MESSAGE_CODE_LIMIT - title.len()
}); });
builder.embed(CreateEmbed::default().title(title).fields(field_iter)) builder.embed(|e| e.title(title).fields(field_iter));
builder
} }
/// Show uploaded sounds /// Show uploaded sounds
@ -43,14 +47,12 @@ 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",
} }
} }
@ -84,20 +86,6 @@ 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,
@ -114,14 +102,15 @@ 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 row = CreateActionRow::Buttons(vec![ let mut row = CreateActionRow::default();
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,
@ -131,8 +120,10 @@ 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),
@ -142,12 +133,16 @@ 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),
@ -157,8 +152,10 @@ 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,
@ -168,14 +165,16 @@ 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 {
CreateEmbed::default() let mut embed = 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))
@ -189,13 +188,15 @@ 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: &ComponentInteraction, interaction: &MessageComponentInteraction,
) -> 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();
@ -204,17 +205,18 @@ 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
.edit_response( .create_interaction_response(&ctx, |r| {
&ctx, r.kind(InteractionResponseType::UpdateMessage)
EditInteractionResponse::default() .interaction_response_data(|d| {
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(())
@ -226,7 +228,6 @@ 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())
@ -234,12 +235,14 @@ impl SoundPager {
} }
}; };
ctx.send( ctx.send(|r| {
CreateReply::default() r.ephemeral(true)
.ephemeral(true) .embed(|e| {
.embed(self.embed(&sounds, count)) *e = self.embed(&sounds, count);
.components(vec![self.create_action_row(count / 25)]), e
) })
.components(|c| c.add_action_row(self.create_action_row(count / 25)))
})
.await?; .await?;
Ok(()) Ok(())
@ -262,7 +265,36 @@ 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(format_search_results(search_results)).await?; ctx.send(|m| {
*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(())
} }

View File

@ -1,7 +1,4 @@
use poise::{ use poise::serenity_prelude::{GuildId, User};
serenity_prelude::{GuildId, User},
CreateReply,
};
use crate::{ use crate::{
cmds::autocomplete_sound, cmds::autocomplete_sound,
@ -63,14 +60,16 @@ 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 permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache()); let guild = ctx.guild().unwrap();
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( ctx.send(|b| {
CreateReply::default() b.ephemeral(true)
.ephemeral(true) .content("Only admins can change other user's greet sounds.")
.content("Only admins can change other user's greet sounds."), })
)
.await?; .await?;
return Ok(()); return Ok(());
@ -110,14 +109,16 @@ 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 permissions = ctx.author_member().await.unwrap().permissions(&ctx.cache()); let guild = ctx.guild().unwrap();
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( ctx.send(|b| {
CreateReply::default() b.ephemeral(true)
.ephemeral(true) .content("Only admins can change other user's greet sounds.")
.content("Only admins can change other user's greet sounds."), })
)
.await?; .await?;
return Ok(()); return Ok(());
@ -158,19 +159,20 @@ 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(CreateReply::default().ephemeral(true).content(format!( ctx.send(|b| {
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( ctx.send(|b| {
CreateReply::default() b.ephemeral(true)
.ephemeral(true) .content("Could not find a sound by that name.")
.content("Could not find a sound by that name."), })
)
.await?; .await?;
} }
} }
@ -185,11 +187,7 @@ 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( ctx.send(|b| b.ephemeral(true).content("Greet sound has been unset"))
CreateReply::default()
.ephemeral(true)
.content("Greet sound has been unset"),
)
.await?; .await?;
Ok(()) Ok(())

View File

@ -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.serenity_context()).await.unwrap(); let songbird = songbird::get(ctx.discord()).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.serenity_context()).await.unwrap(); let songbird = songbird::get(ctx.discord()).await.unwrap();
let _ = songbird.leave(ctx.guild_id().unwrap()).await; let _ = songbird.leave(ctx.guild_id().unwrap()).await;
ctx.say("👍").await?; ctx.say("👍").await?;

View File

@ -1,10 +1,14 @@
use std::{collections::HashMap, env};
use poise::serenity_prelude::{ use poise::serenity_prelude::{
ActionRowComponent, ButtonKind, Context, CreateActionRow, CreateButton, model::{
EditInteractionResponse, FullEvent, Interaction, application::interaction::{Interaction, InteractionResponseType},
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::{
@ -16,29 +20,67 @@ use crate::{
Data, Error, Data, Error,
}; };
pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(), Error> { pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
match event { match event {
FullEvent::VoiceStateUpdate { old, new, .. } => { poise::Event::Ready { .. } => {
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 {
let is_okay = ctx if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
.cache if channel.members(&ctx).await.map(|m| m.len()).unwrap_or(0) <= 1 {
.channel(channel_id)
.map(|c| c.members(&ctx).ok().map(|m| m.len()))
.flatten()
.unwrap_or(0)
<= 1;
if is_okay {
let songbird = songbird::get(ctx).await.unwrap(); let songbird = songbird::get(ctx).await.unwrap();
songbird.remove(guild_id).await?; let _ = 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) {
let guild_data_opt = data.guild_data(guild_id).await; if let Some(guild) = ctx.cache.guild(guild_id) {
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;
@ -65,22 +107,20 @@ pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(
" "
SELECT name, id, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE id = ?", WHERE id = ?
",
join_id join_id
) )
.fetch_one(&data.database) .fetch_one(&data.database)
.await .await
.unwrap(); .unwrap();
let call = join_channel(&ctx, guild_id, user_channel).await?; let (handler, _) = join_channel(&ctx, guild, user_channel).await;
#[cfg(feature = "metrics")]
GREET_COUNTER.inc();
play_audio( play_audio(
&mut sound, &mut sound,
volume, volume,
&mut call.lock().await, &mut handler.lock().await,
&data.database, &data.database,
false, false,
) )
@ -91,104 +131,32 @@ pub async fn listener(ctx: &Context, event: &FullEvent, data: &Data) -> Result<(
} }
} }
} }
FullEvent::InteractionCreate { interaction } => match interaction { }
Interaction::Component(component) => { poise::Event::InteractionCreate { interaction } => match interaction {
Interaction::MessageComponent(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 {
let mode = component.data.custom_id.as_str(); component
match mode { .create_interaction_response(ctx, |r| {
"#stop" => { r.kind(InteractionResponseType::DeferredUpdateMessage)
component.defer(&ctx).await.unwrap(); })
.await
let songbird = songbird::get(ctx).await.unwrap(); .unwrap();
let call_opt = songbird.get(guild_id);
if let Some(call) = call_opt {
let mut lock = call.lock().await;
lock.stop();
}
}
"#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( play_from_query(
&ctx, &ctx,
&data, &data,
&guild, guild_id.to_guild_cached(&ctx).unwrap(),
component.user.id, component.user.id,
None, None,
id.split('#').next().unwrap(), &component.data.custom_id,
mode == "loop", false,
) )
.await; .await;
} }
} }
} }
}
}
_ => {} _ => {}
}, },
_ => {} _ => {}

View File

@ -5,8 +5,6 @@ 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;
@ -14,11 +12,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};
@ -30,6 +28,7 @@ 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>>>,
} }
@ -37,6 +36,36 @@ 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/config.env").exists() { if Path::new("/etc/soundfx-rs/config.env").exists() {
@ -56,7 +85,6 @@ 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(),
@ -64,17 +92,10 @@ 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()
}, },
poise::Command { cmds::search::show_random_sounds(),
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(),
@ -103,7 +124,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}, },
], ],
allowed_mentions: None, allowed_mentions: None,
event_handler: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)), listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),
..Default::default() ..Default::default()
}; };
@ -113,18 +134,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
sqlx::migrate!().run(&database).await?; sqlx::migrate!().run(&database).await?;
#[cfg(feature = "metrics")] poise::Framework::builder()
{ .token(discord_token)
metrics::init_metrics(); .user_data_setup(move |ctx, _bot, framework| {
tokio::spawn(async { metrics::serve().await });
}
let framework = poise::Framework::builder()
.setup(move |ctx, _bot, framework| {
Box::pin(async move { Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?; register_application_commands(ctx, framework, None)
.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(),
@ -132,18 +151,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}) })
}) })
.options(options) .options(options)
.build(); .client_settings(move |client_builder| client_builder.register_songbird())
.intents(GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS)
let mut client = ClientBuilder::new( .run_autosharded()
&discord_token,
GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS,
)
.activity(ActivityData::watching("for /play"))
.framework(framework)
.register_songbird()
.await?; .await?;
client.start_autosharded().await.unwrap();
Ok(()) Ok(())
} }

View File

@ -1,46 +0,0 @@
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(&REGISTRY.gather());
match res_custom {
Ok(s) => s,
Err(e) => {
warn!("Error encoding metrics: {:?}", e);
String::new()
}
}
}

View File

@ -78,10 +78,12 @@ 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;
@ -102,15 +104,17 @@ impl GuildData {
let guild_id = guild_id.into(); let guild_id = guild_id.into();
sqlx::query!( sqlx::query!(
"INSERT INTO servers (id) "
VALUES (?)", INSERT INTO servers (id)
guild_id.get() VALUES (?)
",
guild_id.as_u64()
) )
.execute(db_pool) .execute(db_pool)
.await?; .await?;
Ok(GuildData { Ok(GuildData {
id: guild_id.get(), id: guild_id.as_u64().to_owned(),
prefix: String::from("?"), prefix: String::from("?"),
volume: 100, volume: 100,
allow_greets: AllowGreet::Enabled, allow_greets: AllowGreet::Enabled,

View File

@ -1,5 +1,4 @@
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;
@ -52,9 +51,10 @@ impl JoinSoundCtx for Data {
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(), ",
guild_id.map(|g| g.get()) user_id.as_u64(),
guild_id.map(|g| g.0)
) )
.fetch_one(&self.database) .fetch_one(&self.database)
.await .await
@ -66,9 +66,10 @@ impl JoinSoundCtx for Data {
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(), ",
guild_id.map(|g| g.get()) user_id.as_u64(),
guild_id.map(|g| g.0)
) )
.fetch_one(&self.database) .fetch_one(&self.database)
.await .await
@ -110,29 +111,29 @@ impl JoinSoundCtx for Data {
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.get(), user_id.0,
guild_id.map(|g| g.get()) guild_id.map(|g| g.0)
) )
.execute(transaction.acquire().await?) .execute(&mut transaction)
.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.get(), user_id.0,
join_id, join_id,
guild_id.map(|g| g.get()) guild_id.map(|g| g.0)
) )
.execute(transaction.acquire().await?) .execute(&mut transaction)
.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.get(), user_id.0,
guild_id.map(|g| g.get()) guild_id.map(|g| g.0)
) )
.execute(transaction.acquire().await?) .execute(&mut transaction)
.await?; .await?;
} }
} }

View File

@ -1,7 +1,9 @@
use std::{env, path::Path};
use poise::serenity_prelude::async_trait; use poise::serenity_prelude::async_trait;
use songbird::input::Input; use songbird::input::restartable::Restartable;
use sqlx::Executor; use sqlx::Executor;
use tokio::process::Command; use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database}; use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database};
@ -35,31 +37,17 @@ 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,
@ -103,7 +91,8 @@ impl SoundCtx for Data {
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
@ -127,21 +116,12 @@ impl SoundCtx for Data {
uploader_id = ? OR uploader_id = ? OR
server_id = ? server_id = ?
) )
ORDER BY ORDER BY uploader_id = ? DESC, server_id = ? DESC, public = 1 DESC, rand()
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)
@ -157,21 +137,12 @@ impl SoundCtx for Data {
uploader_id = ? OR uploader_id = ? OR
server_id = ? server_id = ?
) )
ORDER BY ORDER BY uploader_id = ? DESC, server_id = ? DESC, public = 1 DESC, rand()
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)
@ -189,48 +160,18 @@ impl SoundCtx for Data {
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 = ? OR EXISTS( WHERE name LIKE CONCAT(?, '%') AND (uploader_id = ? OR server_id = ?)
SELECT 1 LIMIT 25
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
@ -250,7 +191,8 @@ impl SoundCtx for Data {
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
@ -265,49 +207,8 @@ impl SoundCtx for Data {
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",
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 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)
@ -332,7 +233,8 @@ impl SoundCtx for Data {
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
@ -348,7 +250,8 @@ impl SoundCtx for Data {
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)
@ -369,19 +272,6 @@ impl SoundCtx for Data {
.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,
@ -397,7 +287,7 @@ impl SoundCtx for Data {
} }
impl Sound { impl Sound {
pub(crate) async fn src(&self, db_pool: impl Executor<'_, Database = Database>) -> Vec<u8> { async fn src(&self, db_pool: impl Executor<'_, Database = Database>) -> Vec<u8> {
struct Src { struct Src {
src: Vec<u8>, src: Vec<u8>,
} }
@ -408,7 +298,8 @@ impl Sound {
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)
@ -418,11 +309,33 @@ impl Sound {
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<Input, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> {
Ok(Input::from(self.src(db_pool).await)) let path_name = self.store_sound_source(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>>(
@ -435,7 +348,8 @@ impl Sound {
" "
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)
@ -458,7 +372,8 @@ impl Sound {
FROM sounds FROM sounds
WHERE WHERE
uploader_id = ? AND uploader_id = ? AND
name = ?", name = ?
",
user_id, user_id,
name name
) )
@ -479,7 +394,8 @@ impl Sound {
SET SET
public = ? public = ?
WHERE WHERE
id = ?", id = ?
",
self.public, self.public,
self.id self.id
) )
@ -500,42 +416,6 @@ impl Sound {
Ok(()) 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!(
"INSERT INTO favorite_sounds (user_id, sound_id) VALUES (?, ?)",
user_id,
self.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
)
.execute(db_pool)
.await?;
Ok(())
}
pub async fn create_anon<G: Into<u64>, U: Into<u64>>( pub async fn create_anon<G: Into<u64>, U: Into<u64>>(
name: &str, name: &str,
src_url: &str, src_url: &str,
@ -581,7 +461,8 @@ impl Sound {
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,

View File

@ -1,13 +1,11 @@
use std::{ops::Deref, sync::Arc}; use std::sync::Arc;
use poise::serenity_prelude::{ use poise::serenity_prelude::model::{
model::{ channel::Channel,
guild::Guild, guild::Guild,
id::{ChannelId, UserId}, id::{ChannelId, UserId},
},
ChannelType, EditVoiceState, GuildId,
}; };
use songbird::{tracks::TrackHandle, Call}; use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call};
use sqlx::Executor; use sqlx::Executor;
use tokio::sync::{Mutex, MutexGuard}; use tokio::sync::{Mutex, MutexGuard};
@ -24,20 +22,21 @@ 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>,
r#loop: bool, loop_: bool,
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
let track = sound.playable(db_pool).await?; let (track, track_handler) = create_player(sound.playable(db_pool).await?.into());
let handle = call_handler.play_input(track);
handle.set_volume(volume as f32 / 100.0)?; let _ = track_handler.set_volume(volume as f32 / 100.0);
if r#loop { if loop_ {
handle.enable_loop()?; let _ = track_handler.enable_loop();
} else { } else {
handle.disable_loop()?; let _ = track_handler.disable_loop();
} }
Ok(handle) call_handler.play(track);
Ok(track_handler)
} }
pub async fn queue_audio( pub async fn queue_audio(
@ -47,10 +46,11 @@ 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 track = sound.playable(db_pool).await?; let (a, b) = create_player(sound.playable(db_pool).await?.into());
let handle = call_handler.enqueue_input(track).await;
handle.set_volume(volume as f32 / 100.0)?; let _ = b.set_volume(volume as f32 / 100.0);
call_handler.enqueue(a);
} }
Ok(()) Ok(())
@ -58,65 +58,60 @@ 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_id: GuildId, guild: Guild,
channel_id: ChannelId, channel_id: ChannelId,
) -> Result<Arc<Mutex<Call>>, Box<dyn std::error::Error + Send + Sync>> { ) -> (Arc<Mutex<Call>>, JoinResult<()>) {
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 = ctx let current_voice_state = guild
.cache .voice_states
.guild(guild_id)
.map(|g| {
g.voice_states
.get(&current_user) .get(&current_user)
.and_then(|voice_state| voice_state.channel_id) .and_then(|voice_state| voice_state.channel_id);
})
.flatten();
let call = if current_voice_state == Some(channel_id) { let (call, res) = 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 {
Ok(call) (call, Ok(()))
} else { } else {
songbird.join(guild_id, channel_id).await let (call, res) = songbird.join(guild.id, channel_id).await;
(call, res)
} }
} else { } else {
songbird.join(guild_id, channel_id).await let (call, res) = songbird.join(guild.id, channel_id).await;
}?;
(call, res)
};
{ {
call.lock().await.deafen(true).await?; // set call to deafen
let _ = call.lock().await.deafen(true).await;
} }
if let Some(channel) = ctx.cache.channel(channel_id).map(|c| c.clone()) { if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
if channel.kind == ChannelType::Stage { let _ = channel
let user_id = ctx.cache.current_user().id.clone(); .edit_voice_state(&ctx, ctx.cache.current_user(), |v| v.suppress(false))
.await;
channel
.edit_voice_state(&ctx, user_id, EditVoiceState::new().suppress(true))
.await?;
}
} }
Ok(call) (call, res)
} }
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: impl Deref<Target = Guild> + Send + Sync, guild: Guild,
user_id: UserId, user_id: UserId,
channel: Option<ChannelId>, channel: Option<ChannelId>,
query: &str, query: &str,
r#loop: bool, loop_: bool,
) -> String { ) -> String {
let guild_id = guild.deref().id; let guild_id = guild.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)
@ -134,7 +129,8 @@ pub async fn play_from_query(
match sound_res { match sound_res {
Some(sound) => { Some(sound) => {
{ {
let call_handler = join_channel(ctx, guild_id, user_channel).await.unwrap(); let (call_handler, _) =
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();
@ -145,7 +141,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,
r#loop, loop_,
) )
.await .await
.unwrap(); .unwrap();