restructured a lot. everything updated to poise.

This commit is contained in:
jellywx 2022-01-26 20:33:51 +00:00
parent bb54c0d2c0
commit b350007dae
15 changed files with 775 additions and 757 deletions

View File

@ -1,4 +1,4 @@
use crate::{Context, Error, THEME_COLOR}; use crate::{consts::THEME_COLOR, Context, Error};
/// Get additional information about the bot /// Get additional information about the bot
#[poise::command(slash_command, category = "Information")] #[poise::command(slash_command, category = "Information")]

View File

@ -1,8 +1,14 @@
use std::time::Duration; use std::time::Duration;
use poise::serenity::model::id::{GuildId, RoleId}; use poise::serenity::model::id::{GuildId, RoleId};
use tokio::fs::File;
use crate::{sound::Sound, Context, Error, MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE}; use crate::{
cmds::autocomplete_sound,
consts::{MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE},
models::sound::{Sound, SoundCtx},
Context, Error,
};
/// Upload a new sound to the bot /// Upload a new sound to the bot
#[poise::command(slash_command, rename = "upload", category = "Manage")] #[poise::command(slash_command, rename = "upload", category = "Manage")]
@ -123,14 +129,16 @@ pub async fn upload_new_sound(
#[poise::command(slash_command, rename = "delete", category = "Manage")] #[poise::command(slash_command, rename = "delete", category = "Manage")]
pub async fn delete_sound( pub async fn delete_sound(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Name or ID of sound to delete"] name: String, #[description = "Name or ID of sound to delete"]
#[autocomplete = "autocomplete_sound"]
name: String,
) -> 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.0;
let gid = ctx.guild_id().unwrap().0; let gid = ctx.guild_id().unwrap().0;
let sound_vec = Sound::search_for_sound(&name, gid, uid, pool.clone(), 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();
match sound_result { match sound_result {
@ -174,14 +182,16 @@ pub async fn delete_sound(
#[poise::command(slash_command, rename = "public", category = "Manage")] #[poise::command(slash_command, rename = "public", category = "Manage")]
pub async fn change_public( pub async fn change_public(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Name or ID of sound to change privacy setting of"] name: String, #[description = "Name or ID of sound to change privacy setting of"]
#[autocomplete = "autocomplete_sound"]
name: String,
) -> 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.0;
let gid = ctx.guild_id().unwrap().0; let gid = ctx.guild_id().unwrap().0;
let mut sound_vec = Sound::search_for_sound(&name, gid, uid, pool.clone(), 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();
match sound_result { match sound_result {
@ -210,3 +220,39 @@ pub async fn change_public(
Ok(()) Ok(())
} }
/// Download a sound file from the bot
#[poise::command(slash_command, rename = "download", category = "Manage")]
pub async fn download_file(
ctx: Context<'_>,
#[description = "Name or ID of sound to download"]
#[autocomplete = "autocomplete_sound"]
name: String,
) -> Result<(), Error> {
ctx.defer().await?;
let sound = ctx
.data()
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
.await?;
match sound.first() {
Some(sound) => {
let source = sound
.store_sound_source(ctx.data().database.clone())
.await?;
let file = File::open(&source).await?;
let name = format!("{}-{}.opus", sound.id, sound.name);
ctx.send(|m| m.attachment((&file, name.as_str()).into()))
.await?;
}
None => {
ctx.say("No sound found by specified name/ID").await?;
}
}
Ok(())
}

View File

@ -1,6 +1,24 @@
use crate::{models::sound::SoundCtx, Context};
pub mod info; pub mod info;
pub mod manage; pub mod manage;
pub mod play; pub mod play;
// pub mod search; pub mod search;
// pub mod settings; pub mod settings;
// pub mod stop; pub mod stop;
pub async fn autocomplete_sound(
ctx: Context<'_>,
partial: String,
) -> Vec<poise::AutocompleteChoice<String>> {
ctx.data()
.autocomplete_user_sounds(&partial, ctx.author().id, ctx.guild_id().unwrap())
.await
.unwrap_or(vec![])
.iter()
.map(|s| poise::AutocompleteChoice {
name: s.name.clone(),
value: s.id.to_string(),
})
.collect()
}

View File

@ -2,31 +2,57 @@ use poise::serenity::{
builder::CreateActionRow, model::interactions::message_component::ButtonStyle, builder::CreateActionRow, model::interactions::message_component::ButtonStyle,
}; };
use crate::{play_from_query, sound::Sound, Context, Error}; use crate::{
cmds::autocomplete_sound, models::sound::SoundCtx, utils::play_from_query, Context, Error,
};
/// Play a sound in your current voice channel /// Play a sound in your current voice channel
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn play( pub async fn play(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Name or ID of sound to play"] name: String, #[description = "Name or ID of sound to play"]
#[autocomplete = "autocomplete_sound"]
name: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let guild = ctx.guild().unwrap(); let guild = ctx.guild().unwrap();
ctx.say(play_from_query(&ctx, guild, ctx.author().id, &name, false).await) ctx.say(
play_from_query(
&ctx.discord(),
&ctx.data(),
guild,
ctx.author().id,
&name,
false,
)
.await,
)
.await?; .await?;
Ok(()) Ok(())
} }
/// Loop a sound in your current voice channel /// Loop a sound in your current voice channel
#[poise::command(slash_command)] #[poise::command(slash_command, rename = "loop")]
pub async fn loop_play( pub async fn loop_play(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Name or ID of sound to loop"] name: String, #[description = "Name or ID of sound to loop"]
#[autocomplete = "autocomplete_sound"]
name: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let guild = ctx.guild().unwrap(); let guild = ctx.guild().unwrap();
ctx.say(play_from_query(&ctx, guild, ctx.author().id, &name, true).await) ctx.say(
play_from_query(
&ctx.discord(),
&ctx.data(),
guild,
ctx.author().id,
&name,
true,
)
.await,
)
.await?; .await?;
Ok(()) Ok(())
@ -36,36 +62,84 @@ pub async fn loop_play(
#[poise::command(slash_command, rename = "soundboard", category = "Play")] #[poise::command(slash_command, rename = "soundboard", category = "Play")]
pub async fn soundboard( pub async fn soundboard(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Name or ID of sound for button 1"] sound_1: String, #[description = "Name or ID of sound for button 1"]
#[description = "Name or ID of sound for button 2"] sound_2: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 3"] sound_3: Option<String>, sound_1: String,
#[description = "Name or ID of sound for button 4"] sound_4: Option<String>, #[description = "Name or ID of sound for button 2"]
#[description = "Name or ID of sound for button 5"] sound_5: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 6"] sound_6: Option<String>, sound_2: Option<String>,
#[description = "Name or ID of sound for button 7"] sound_7: Option<String>, #[description = "Name or ID of sound for button 3"]
#[description = "Name or ID of sound for button 8"] sound_8: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 9"] sound_9: Option<String>, sound_3: Option<String>,
#[description = "Name or ID of sound for button 10"] sound_10: Option<String>, #[description = "Name or ID of sound for button 4"]
#[description = "Name or ID of sound for button 11"] sound_11: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 12"] sound_12: Option<String>, sound_4: Option<String>,
#[description = "Name or ID of sound for button 13"] sound_13: Option<String>, #[description = "Name or ID of sound for button 5"]
#[description = "Name or ID of sound for button 14"] sound_14: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 15"] sound_15: Option<String>, sound_5: Option<String>,
#[description = "Name or ID of sound for button 16"] sound_16: Option<String>, #[description = "Name or ID of sound for button 6"]
#[description = "Name or ID of sound for button 17"] sound_17: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 18"] sound_18: Option<String>, sound_6: Option<String>,
#[description = "Name or ID of sound for button 19"] sound_19: Option<String>, #[description = "Name or ID of sound for button 7"]
#[description = "Name or ID of sound for button 20"] sound_20: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 21"] sound_21: Option<String>, sound_7: Option<String>,
#[description = "Name or ID of sound for button 22"] sound_22: Option<String>, #[description = "Name or ID of sound for button 8"]
#[description = "Name or ID of sound for button 23"] sound_23: Option<String>, #[autocomplete = "autocomplete_sound"]
#[description = "Name or ID of sound for button 24"] sound_24: Option<String>, sound_8: Option<String>,
#[description = "Name or ID of sound for button 25"] sound_25: Option<String>, #[description = "Name or ID of sound for button 9"]
#[autocomplete = "autocomplete_sound"]
sound_9: Option<String>,
#[description = "Name or ID of sound for button 10"]
#[autocomplete = "autocomplete_sound"]
sound_10: Option<String>,
#[description = "Name or ID of sound for button 11"]
#[autocomplete = "autocomplete_sound"]
sound_11: Option<String>,
#[description = "Name or ID of sound for button 12"]
#[autocomplete = "autocomplete_sound"]
sound_12: Option<String>,
#[description = "Name or ID of sound for button 13"]
#[autocomplete = "autocomplete_sound"]
sound_13: Option<String>,
#[description = "Name or ID of sound for button 14"]
#[autocomplete = "autocomplete_sound"]
sound_14: Option<String>,
#[description = "Name or ID of sound for button 15"]
#[autocomplete = "autocomplete_sound"]
sound_15: Option<String>,
#[description = "Name or ID of sound for button 16"]
#[autocomplete = "autocomplete_sound"]
sound_16: Option<String>,
#[description = "Name or ID of sound for button 17"]
#[autocomplete = "autocomplete_sound"]
sound_17: Option<String>,
#[description = "Name or ID of sound for button 18"]
#[autocomplete = "autocomplete_sound"]
sound_18: Option<String>,
#[description = "Name or ID of sound for button 19"]
#[autocomplete = "autocomplete_sound"]
sound_19: Option<String>,
#[description = "Name or ID of sound for button 20"]
#[autocomplete = "autocomplete_sound"]
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?;
let pool = ctx.data().database.clone();
let query_terms = [ let query_terms = [
Some(sound_1), Some(sound_1),
sound_2, sound_2,
@ -97,13 +171,9 @@ pub async fn soundboard(
let mut sounds = vec![]; let mut sounds = vec![];
for sound in query_terms.iter().flatten() { for sound in query_terms.iter().flatten() {
let search = Sound::search_for_sound( let search = ctx
&sound, .data()
ctx.guild_id().unwrap(), .search_for_sound(&sound, ctx.guild_id().unwrap(), ctx.author().id, true)
ctx.author().id,
pool.clone(),
true,
)
.await?; .await?;
if let Some(sound) = search.first() { if let Some(sound) = search.first() {

View File

@ -1,6 +1,13 @@
use crate::sound::Sound; use poise::{serenity::constants::MESSAGE_CODE_LIMIT, CreateReply};
use crate::{
models::sound::{Sound, SoundCtx},
Context, Error,
};
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
let mut builder = CreateReply::default();
fn format_search_results(search_results: Vec<Sound>) -> CreateGenericResponse {
let mut current_character_count = 0; let mut current_character_count = 0;
let title = "Public sounds matching filter:"; let title = "Public sounds matching filter:";
@ -11,49 +18,25 @@ fn format_search_results(search_results: Vec<Sound>) -> CreateGenericResponse {
.filter(|item| { .filter(|item| {
current_character_count += item.0.len() + item.1.len(); current_character_count += item.0.len() + item.1.len();
current_character_count <= serenity::constants::MESSAGE_CODE_LIMIT - title.len() current_character_count <= MESSAGE_CODE_LIMIT - title.len()
}); });
CreateGenericResponse::new().embed(|e| e.title(title).fields(field_iter)) builder.embed(|e| e.title(title).fields(field_iter));
builder
} }
#[command("list")] /// Show the sounds uploaded to this server
#[group("Search")] #[poise::command(slash_command, rename = "list")]
#[description("Show the sounds uploaded by you or to your server")] pub async fn list_sounds(ctx: Context<'_>) -> Result<(), Error> {
#[arg(
name = "me",
description = "Whether to list your sounds or server sounds (default: server)",
kind = "Boolean",
required = false
)]
#[example("`/list` - list sounds uploaded to the server you're in")]
#[example("`/list [me: True]` - list sounds you have uploaded across all servers")]
pub async fn list_sounds(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
args: Args,
) -> CommandResult {
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let sounds; let sounds;
let mut message_buffer; let mut message_buffer;
if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) { sounds = ctx.data().guild_sounds(ctx.guild_id().unwrap()).await?;
sounds = Sound::user_sounds(invoke.author_id(), pool).await?;
message_buffer = "All your sounds: ".to_string(); message_buffer = "Sounds on this server: ".to_string();
} else {
sounds = Sound::guild_sounds(invoke.guild_id().unwrap(), pool).await?;
message_buffer = "All sounds on this server: ".to_string();
}
// todo change this to iterator
for sound in sounds { for sound in sounds {
message_buffer.push_str( message_buffer.push_str(
format!( format!(
@ -65,85 +48,77 @@ pub async fn list_sounds(
); );
if message_buffer.len() > 2000 { if message_buffer.len() > 2000 {
invoke ctx.say(message_buffer).await?;
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(message_buffer),
)
.await?;
message_buffer = "".to_string(); message_buffer = "".to_string();
} }
} }
if message_buffer.len() > 0 { if message_buffer.len() > 0 {
invoke ctx.say(message_buffer).await?;
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(message_buffer),
)
.await?;
} }
Ok(()) Ok(())
} }
#[command("search")] /// Show all sounds you have uploaded
#[group("Search")] #[poise::command(slash_command, rename = "me")]
#[description("Search for sounds")] pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> {
#[arg( let sounds;
name = "query", let mut message_buffer;
kind = "String",
description = "Sound name to search for",
required = true
)]
pub async fn search_sounds(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
args: Args,
) -> CommandResult {
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let query = args.named("query").unwrap(); sounds = ctx.data().user_sounds(ctx.author().id).await?;
let search_results = Sound::search_for_sound( message_buffer = "Sounds on this server: ".to_string();
query,
invoke.guild_id().unwrap(), // todo change this to iterator
invoke.author_id(), for sound in sounds {
pool, message_buffer.push_str(
false, format!(
"**{}** ({}), ",
sound.name,
if sound.public { "🔓" } else { "🔒" }
) )
.as_str(),
);
if message_buffer.len() > 2000 {
ctx.say(message_buffer).await?;
message_buffer = "".to_string();
}
}
if message_buffer.len() > 0 {
ctx.say(message_buffer).await?;
}
Ok(())
}
/// Search for sounds
#[poise::command(slash_command, rename = "search", category = "Search")]
pub async fn search_sounds(
ctx: Context<'_>,
#[description = "Sound name to search for"] query: String,
) -> Result<(), Error> {
let search_results = ctx
.data()
.search_for_sound(&query, ctx.guild_id().unwrap(), ctx.author().id, false)
.await?; .await?;
invoke ctx.send(|m| {
.respond(ctx.http.clone(), format_search_results(search_results)) *m = format_search_results(search_results);
m
})
.await?; .await?;
Ok(()) Ok(())
} }
#[command("random")] /// Show a page of random sounds
#[group("Search")] #[poise::command(slash_command, rename = "random")]
#[description("Show a page of random sounds")] pub async fn show_random_sounds(ctx: Context<'_>) -> Result<(), Error> {
pub async fn show_random_sounds(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
_args: Args,
) -> CommandResult {
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let search_results = sqlx::query_as_unchecked!( let search_results = sqlx::query_as_unchecked!(
Sound, Sound,
" "
@ -154,12 +129,13 @@ SELECT name, id, public, server_id, uploader_id
LIMIT 25 LIMIT 25
" "
) )
.fetch_all(&pool) .fetch_all(&ctx.data().database)
.await .await?;
.unwrap();
invoke ctx.send(|m| {
.respond(ctx.http.clone(), format_search_results(search_results)) *m = format_search_results(search_results);
m
})
.await?; .await?;
Ok(()) Ok(())

View File

@ -1,307 +1,126 @@
use regex_command_attr::command;
use serenity::{client::Context, framework::standard::CommandResult};
use crate::{ use crate::{
framework::{Args, CommandInvoke, CreateGenericResponse}, models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::SoundCtx},
guild_data::CtxGuildData, Context, Error,
sound::{JoinSoundCtx, Sound},
MySQL,
}; };
#[command("volume")] /// Change the bot's volume in this server
#[aliases("vol")] #[poise::command(slash_command, rename = "volume")]
#[required_permissions(Managed)]
#[group("Settings")]
#[description("Change the bot's volume in this server")]
#[arg(
name = "volume",
description = "New volume for the bot to use",
kind = "Integer",
required = false
)]
#[example("`/volume` - check the volume on the current server")]
#[example("`/volume 100` - reset the volume on the current server")]
#[example("`/volume 10` - set the volume on the current server to 10%")]
pub async fn change_volume( pub async fn change_volume(
ctx: &Context, ctx: Context<'_>,
invoke: &(dyn CommandInvoke + Sync + Send), #[description = "New volume as a percentage"] volume: Option<usize>,
args: Args, ) -> Result<(), Error> {
) -> CommandResult { let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await;
let guild_data = guild_data_opt.unwrap(); let guild_data = guild_data_opt.unwrap();
if let Some(volume) = args.named("volume").map(|i| i.parse::<u8>().ok()).flatten() { if let Some(volume) = volume {
guild_data.write().await.volume = volume; guild_data.write().await.volume = volume as u8;
guild_data.read().await.commit(pool).await?; guild_data
.read()
invoke .await
.respond( .commit(ctx.data().database.clone())
ctx.http.clone(),
CreateGenericResponse::new().content(format!("Volume changed to {}%", volume)),
)
.await?; .await?;
ctx.say(format!("Volume changed to {}%", volume)).await?;
} else { } else {
let read = guild_data.read().await; let read = guild_data.read().await;
invoke ctx.say(format!(
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(format!(
"Current server volume: {vol}%. Change the volume with `/volume <new volume>`", "Current server volume: {vol}%. Change the volume with `/volume <new volume>`",
vol = read.volume vol = read.volume
)), ))
)
.await?; .await?;
} }
Ok(()) Ok(())
} }
#[command("prefix")] /// Manage greet sounds on this server
#[required_permissions(Restricted)] #[poise::command(slash_command, rename = "greet")]
#[kind(Text)] pub async fn greet_sound(_ctx: Context<'_>) -> Result<(), Error> {
#[group("Settings")]
#[description("Change the prefix of the bot for using non-slash commands")]
#[arg(
name = "prefix",
kind = "String",
description = "The new prefix to use for the bot",
required = true
)]
pub async fn change_prefix(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
args: Args,
) -> CommandResult {
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let guild_data;
{
let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await;
guild_data = guild_data_opt.unwrap();
}
if let Some(prefix) = args.named("prefix") {
if prefix.len() <= 5 && !prefix.is_empty() {
let reply = format!("Prefix changed to `{}`", prefix);
{
guild_data.write().await.prefix = prefix.to_string();
}
{
let read = guild_data.read().await;
read.commit(pool).await?;
}
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(reply),
)
.await?;
} else {
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Prefix must be less than 5 characters long"),
)
.await?;
}
} else {
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(format!(
"Usage: `{prefix}prefix <new prefix>`",
prefix = guild_data.read().await.prefix
)),
)
.await?;
}
Ok(()) Ok(())
} }
#[command("roles")] /// Set a join sound
#[required_permissions(Restricted)] #[poise::command(slash_command, rename = "set")]
#[group("Settings")]
#[description("Change the role allowed to use the bot")]
#[arg(
name = "role",
kind = "Role",
description = "A role to allow to use the bot. Use @everyone to allow all server members",
required = true
)]
#[example("`/roles @everyone` - allow all server members to use the bot")]
#[example("`/roles @DJ` - allow only server members with the 'DJ' role to use the bot")]
pub async fn set_allowed_roles(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
args: Args,
) -> CommandResult {
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let role_id = args.named("role").unwrap().parse::<u64>().unwrap();
let guild_data = ctx.guild_data(invoke.guild_id().unwrap()).await.unwrap();
guild_data.write().await.allowed_role = Some(role_id);
guild_data.read().await.commit(pool).await?;
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(format!("Allowed role set to <@&{}>", role_id)),
)
.await?;
Ok(())
}
#[command("greet")]
#[group("Settings")]
#[description("Set a join sound")]
#[arg(
name = "query",
kind = "String",
description = "Name or ID of sound to set as your greet sound",
required = false
)]
#[example("`/greet` - remove your join sound")]
#[example("`/greet 1523` - set your join sound to sound with ID 1523")]
pub async fn set_greet_sound( pub async fn set_greet_sound(
ctx: &Context, ctx: Context<'_>,
invoke: &(dyn CommandInvoke + Sync + Send), #[description = "Name or ID of sound to set as your join sound"] name: String,
args: Args, ) -> Result<(), Error> {
) -> CommandResult { let sound_vec = ctx
let pool = ctx .data()
.data .search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not get SQLPool from data");
let query = args
.named("query")
.map(|s| s.to_owned())
.unwrap_or(String::new());
let user_id = invoke.author_id();
if query.len() == 0 {
ctx.update_join_sound(user_id, None).await;
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content("Your greet sound has been unset."),
)
.await?;
} else {
let sound_vec = Sound::search_for_sound(
&query,
invoke.guild_id().unwrap(),
user_id,
pool.clone(),
true,
)
.await?; .await?;
match sound_vec.first() { match sound_vec.first() {
Some(sound) => { Some(sound) => {
ctx.update_join_sound(user_id, Some(sound.id)).await; ctx.data()
.update_join_sound(ctx.author().id, Some(sound.id))
.await;
invoke ctx.say(format!(
.respond(
ctx.http.clone(),
CreateGenericResponse::new().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 => {
invoke ctx.say("Could not find a sound by that name.").await?;
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Could not find a sound by that name."),
)
.await?;
}
} }
} }
Ok(()) Ok(())
} }
#[command("allow_greet")] /// Set a join sound
#[group("Settings")] #[poise::command(slash_command, rename = "unset")]
#[description("Configure whether users should be able to use join sounds")] pub async fn unset_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
#[required_permissions(Restricted)] ctx.data().update_join_sound(ctx.author().id, None).await;
#[example("`/allow_greet` - disable greet sounds in the server")]
#[example("`/allow_greet` - re-enable greet sounds in the server")]
pub async fn allow_greet_sounds(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
_args: Args,
) -> CommandResult {
let pool = ctx
.data
.read()
.await
.get::<MySQL>()
.cloned()
.expect("Could not acquire SQL pool from data");
let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await; ctx.say("Greet sound has been unset").await?;
Ok(())
}
/// Disable greet sounds on this server
#[poise::command(slash_command, rename = "disable")]
pub async fn disable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
if let Ok(guild_data) = guild_data_opt { if let Ok(guild_data) = guild_data_opt {
let current = guild_data.read().await.allow_greets; guild_data.write().await.allow_greets = false;
{ guild_data
guild_data.write().await.allow_greets = !current; .read()
} .await
.commit(ctx.data().database.clone())
guild_data.read().await.commit(pool).await?;
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(format!(
"Greet sounds have been {}abled in this server",
if !current { "en" } else { "dis" }
)),
)
.await?; .await?;
} }
ctx.say("Greet sounds have been disabled in this server")
.await?;
Ok(())
}
/// Enable greet sounds on this server
#[poise::command(slash_command, rename = "enable")]
pub async fn enable_greet_sound(ctx: Context<'_>) -> Result<(), Error> {
let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await;
if let Ok(guild_data) = guild_data_opt {
guild_data.write().await.allow_greets = true;
guild_data
.read()
.await
.commit(ctx.data().database.clone())
.await?;
}
ctx.say("Greet sounds have been enable in this server")
.await?;
Ok(()) Ok(())
} }

View File

@ -1,22 +1,12 @@
use regex_command_attr::command;
use serenity::{client::Context, framework::standard::CommandResult};
use songbird; use songbird;
use crate::framework::{Args, CommandInvoke, CreateGenericResponse}; use crate::{Context, Error};
#[command("stop")] /// Stop the bot from playing
#[required_permissions(Managed)] #[poise::command(slash_command, rename = "stop")]
#[group("Stop")] pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> {
#[description("Stop the bot from playing")] let songbird = songbird::get(ctx.discord()).await.unwrap();
pub async fn stop_playing( let call_opt = songbird.get(ctx.guild_id().unwrap());
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
_args: Args,
) -> CommandResult {
let guild_id = invoke.guild_id().unwrap();
let songbird = songbird::get(ctx).await.unwrap();
let call_opt = songbird.get(guild_id);
if let Some(call) = call_opt { if let Some(call) = call_opt {
let mut lock = call.lock().await; let mut lock = call.lock().await;
@ -24,31 +14,18 @@ pub async fn stop_playing(
lock.stop(); lock.stop();
} }
invoke ctx.say("👍").await?;
.respond(ctx.http.clone(), CreateGenericResponse::new().content("👍"))
.await?;
Ok(()) Ok(())
} }
#[command] /// Disconnect the bot
#[aliases("dc")] #[poise::command(slash_command)]
#[required_permissions(Managed)] pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> {
#[group("Stop")] let songbird = songbird::get(ctx.discord()).await.unwrap();
#[description("Disconnect the bot")] let _ = songbird.leave(ctx.guild_id().unwrap()).await;
pub async fn disconnect(
ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send),
_args: Args,
) -> CommandResult {
let guild_id = invoke.guild_id().unwrap();
let songbird = songbird::get(ctx).await.unwrap(); ctx.say("👍").await?;
let _ = songbird.leave(guild_id).await;
invoke
.respond(ctx.http.clone(), CreateGenericResponse::new().content("👍"))
.await?;
Ok(()) Ok(())
} }

9
src/consts.rs Normal file
View File

@ -0,0 +1,9 @@
use std::env;
pub const THEME_COLOR: u32 = 0x00e0f3;
lazy_static! {
pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS").unwrap().parse::<u32>().unwrap();
pub static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::<u64>().unwrap();
pub static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap();
}

View File

@ -1,27 +1,19 @@
use std::{collections::HashMap, env}; use std::{collections::HashMap, env};
use poise::serenity::{async_trait, model::channel::Channel, prelude::Context, utils::shard_id}; use poise::serenity::{
use songbird::{Event, EventContext, EventHandler as SongbirdEventHandler}; model::{
channel::Channel,
use crate::{ interactions::{Interaction, InteractionResponseType},
guild_data::CtxGuildData, },
join_channel, play_audio, prelude::Context,
sound::{JoinSoundCtx, Sound}, utils::shard_id,
Data, Error,
}; };
pub struct RestartTrack; use crate::{
models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::Sound},
#[async_trait] utils::{join_channel, play_audio, play_from_query},
impl SongbirdEventHandler for RestartTrack { Data, Error,
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> { };
if let EventContext::Track(&[(_state, track)]) = ctx {
let _ = track.seek_time(Default::default());
}
None
}
}
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> { pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
match event { match event {
@ -126,6 +118,29 @@ SELECT name, id, public, server_id, uploader_id
} }
} }
} }
poise::Event::InteractionCreate { interaction } => match interaction {
Interaction::MessageComponent(component) => {
if component.guild_id.is_some() {
play_from_query(
&ctx,
&data,
component.guild_id.unwrap().to_guild_cached(&ctx).unwrap(),
component.user.id,
&component.data.custom_id,
false,
)
.await;
component
.create_interaction_response(ctx, |r| {
r.kind(InteractionResponseType::DeferredUpdateMessage)
})
.await
.unwrap();
}
}
_ => {}
},
_ => {} _ => {}
} }

View File

@ -2,10 +2,11 @@
extern crate lazy_static; extern crate lazy_static;
mod cmds; mod cmds;
mod consts;
mod error; mod error;
mod event_handlers; mod event_handlers;
mod guild_data; mod models;
mod sound; mod utils;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
@ -14,21 +15,15 @@ use dotenv::dotenv;
use poise::serenity::{ use poise::serenity::{
builder::CreateApplicationCommands, builder::CreateApplicationCommands,
model::{ model::{
channel::Channel,
gateway::{Activity, GatewayIntents}, gateway::{Activity, GatewayIntents},
guild::Guild, id::{GuildId, UserId},
id::{ChannelId, GuildId, UserId},
}, },
}; };
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call, SerenityInit}; use songbird::SerenityInit;
use sqlx::mysql::MySqlPool; use sqlx::mysql::MySqlPool;
use tokio::sync::{Mutex, MutexGuard, RwLock}; use tokio::sync::RwLock;
use crate::{ use crate::{event_handlers::listener, models::guild_data::GuildData};
event_handlers::listener,
guild_data::{CtxGuildData, GuildData},
sound::Sound,
};
pub struct Data { pub struct Data {
database: MySqlPool, database: MySqlPool,
@ -40,137 +35,6 @@ 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>;
const THEME_COLOR: u32 = 0x00e0f3;
lazy_static! {
static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS").unwrap().parse::<u32>().unwrap();
static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::<u64>().unwrap();
static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap();
}
async fn play_audio(
sound: &mut Sound,
volume: u8,
call_handler: &mut MutexGuard<'_, Call>,
mysql_pool: MySqlPool,
loop_: bool,
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
let (track, track_handler) =
create_player(sound.store_sound_source(mysql_pool.clone()).await?.into());
let _ = track_handler.set_volume(volume as f32 / 100.0);
if loop_ {
let _ = track_handler.enable_loop();
} else {
let _ = track_handler.disable_loop();
}
call_handler.play(track);
Ok(track_handler)
}
async fn join_channel(
ctx: &poise::serenity_prelude::Context,
guild: Guild,
channel_id: ChannelId,
) -> (Arc<Mutex<Call>>, JoinResult<()>) {
let songbird = songbird::get(ctx).await.unwrap();
let current_user = ctx.cache.current_user_id();
let current_voice_state = guild
.voice_states
.get(&current_user)
.and_then(|voice_state| voice_state.channel_id);
let (call, res) = if current_voice_state == Some(channel_id) {
let call_opt = songbird.get(guild.id);
if let Some(call) = call_opt {
(call, Ok(()))
} else {
let (call, res) = songbird.join(guild.id, channel_id).await;
(call, res)
}
} else {
let (call, res) = songbird.join(guild.id, channel_id).await;
(call, res)
};
{
// set call to deafen
let _ = call.lock().await.deafen(true).await;
}
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
channel
.edit_voice_state(&ctx, ctx.cache.current_user(), |v| v.suppress(false))
.await;
}
(call, res)
}
async fn play_from_query(
ctx: &Context<'_>,
guild: Guild,
user_id: UserId,
query: &str,
loop_: bool,
) -> String {
let guild_id = guild.id;
let channel_to_join = guild
.voice_states
.get(&user_id)
.and_then(|voice_state| voice_state.channel_id);
match channel_to_join {
Some(user_channel) => {
let pool = ctx.data().database.clone();
let mut sound_vec =
Sound::search_for_sound(query, guild_id, user_id, pool.clone(), true)
.await
.unwrap();
let sound_res = sound_vec.first_mut();
match sound_res {
Some(sound) => {
{
let (call_handler, _) =
join_channel(ctx.discord(), guild.clone(), user_channel).await;
let guild_data = ctx.guild_data(guild_id).await.unwrap();
let mut lock = call_handler.lock().await;
play_audio(
sound,
guild_data.read().await.volume,
&mut lock,
pool,
loop_,
)
.await
.unwrap();
}
format!("Playing sound {} with ID {}", sound.name, sound.id)
}
None => "Couldn't find sound by term provided".to_string(),
}
}
None => "You are not in a voice chat!".to_string(),
}
}
pub async fn register_application_commands( pub async fn register_application_commands(
ctx: &poise::serenity::client::Context, ctx: &poise::serenity::client::Context,
framework: &poise::Framework<Data, Error>, framework: &poise::Framework<Data, Error>,
@ -215,10 +79,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
cmds::info::info(), cmds::info::info(),
cmds::manage::change_public(), cmds::manage::change_public(),
cmds::manage::upload_new_sound(), cmds::manage::upload_new_sound(),
cmds::manage::download_file(),
cmds::manage::delete_sound(), cmds::manage::delete_sound(),
cmds::play::play(), cmds::play::play(),
cmds::play::loop_play(), cmds::play::loop_play(),
cmds::play::soundboard(), cmds::play::soundboard(),
cmds::search::list_sounds(),
cmds::search::list_user_sounds(),
cmds::search::show_random_sounds(),
cmds::search::search_sounds(),
cmds::stop::stop_playing(),
cmds::stop::disconnect(),
cmds::settings::change_volume(),
poise::Command {
subcommands: vec![
cmds::settings::disable_greet_sound(),
cmds::settings::enable_greet_sound(),
cmds::settings::set_greet_sound(),
cmds::settings::unset_greet_sound(),
cmds::settings::greet_sound(),
],
..cmds::settings::greet_sound()
},
], ],
allowed_mentions: None, allowed_mentions: None,
listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)), listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),

87
src/models/join_sound.rs Normal file
View File

@ -0,0 +1,87 @@
use poise::serenity::{async_trait, model::id::UserId};
use crate::Data;
#[async_trait]
pub trait JoinSoundCtx {
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32>;
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
&self,
user_id: U,
join_id: Option<u32>,
);
}
#[async_trait]
impl JoinSoundCtx for Data {
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32> {
let user_id = user_id.into();
let x = if let Some(join_sound_id) = self.join_sound_cache.get(&user_id) {
join_sound_id.value().clone()
} else {
let join_sound_id = {
let pool = self.database.clone();
let join_id_res = sqlx::query!(
"
SELECT join_sound_id
FROM users
WHERE user = ?
",
user_id.as_u64()
)
.fetch_one(&pool)
.await;
if let Ok(row) = join_id_res {
row.join_sound_id
} else {
None
}
};
self.join_sound_cache.insert(user_id, join_sound_id);
join_sound_id
};
x
}
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
&self,
user_id: U,
join_id: Option<u32>,
) {
let user_id = user_id.into();
self.join_sound_cache.insert(user_id, join_id);
let pool = self.database.clone();
let _ = sqlx::query!(
"
INSERT IGNORE INTO users (user)
VALUES (?)
",
user_id.as_u64()
)
.execute(&pool)
.await;
let _ = sqlx::query!(
"
UPDATE users
SET
join_sound_id = ?
WHERE
user = ?
",
join_id,
user_id.as_u64()
)
.execute(&pool)
.await;
}
}

3
src/models/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod guild_data;
pub mod join_sound;
pub mod sound;

View File

@ -1,96 +1,11 @@
use std::{env, path::Path}; use std::{env, path::Path};
use poise::serenity::{async_trait, model::id::UserId}; use poise::serenity::async_trait;
use songbird::input::restartable::Restartable; use songbird::input::restartable::Restartable;
use sqlx::mysql::MySqlPool; use sqlx::{mysql::MySqlPool, Error};
use tokio::{fs::File, io::AsyncWriteExt, process::Command}; use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use super::error::ErrorTypes; use crate::{error::ErrorTypes, Data};
use crate::Data;
#[async_trait]
pub trait JoinSoundCtx {
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32>;
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
&self,
user_id: U,
join_id: Option<u32>,
);
}
#[async_trait]
impl JoinSoundCtx for Data {
async fn join_sound<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Option<u32> {
let user_id = user_id.into();
let x = if let Some(join_sound_id) = self.join_sound_cache.get(&user_id) {
join_sound_id.value().clone()
} else {
let join_sound_id = {
let pool = self.database.clone();
let join_id_res = sqlx::query!(
"
SELECT join_sound_id
FROM users
WHERE user = ?
",
user_id.as_u64()
)
.fetch_one(&pool)
.await;
if let Ok(row) = join_id_res {
row.join_sound_id
} else {
None
}
};
self.join_sound_cache.insert(user_id, join_sound_id);
join_sound_id
};
x
}
async fn update_join_sound<U: Into<UserId> + Send + Sync>(
&self,
user_id: U,
join_id: Option<u32>,
) {
let user_id = user_id.into();
self.join_sound_cache.insert(user_id, join_id);
let pool = self.database.clone();
let _ = sqlx::query!(
"
INSERT IGNORE INTO users (user)
VALUES (?)
",
user_id.as_u64()
)
.execute(&pool)
.await;
let _ = sqlx::query!(
"
UPDATE users
SET
join_sound_id = ?
WHERE
user = ?
",
join_id,
user_id.as_u64()
)
.execute(&pool)
.await;
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Sound { pub struct Sound {
@ -107,16 +22,41 @@ impl PartialEq for Sound {
} }
} }
impl Sound { #[async_trait]
pub async fn search_for_sound<G: Into<u64>, U: Into<u64>>( pub trait SoundCtx {
async fn search_for_sound<G: Into<u64> + Send, U: Into<u64> + Send>(
&self,
query: &str,
guild_id: G,
user_id: U,
strict: bool,
) -> Result<Vec<Sound>, sqlx::Error>;
async fn autocomplete_user_sounds<U: Into<u64> + Send, G: Into<u64> + Send>(
&self,
query: &str,
user_id: U,
guild_id: G,
) -> Result<Vec<Sound>, sqlx::Error>;
async fn user_sounds<U: Into<u64> + Send>(&self, user_id: U)
-> Result<Vec<Sound>, sqlx::Error>;
async fn guild_sounds<G: Into<u64> + Send>(
&self,
guild_id: G,
) -> Result<Vec<Sound>, sqlx::Error>;
}
#[async_trait]
impl SoundCtx for Data {
async fn search_for_sound<G: Into<u64> + Send, U: Into<u64> + Send>(
&self,
query: &str, query: &str,
guild_id: G, guild_id: G,
user_id: U, user_id: U,
db_pool: MySqlPool,
strict: bool, strict: bool,
) -> Result<Vec<Sound>, sqlx::Error> { ) -> Result<Vec<Sound>, sqlx::Error> {
let guild_id = guild_id.into(); let guild_id = guild_id.into();
let user_id = user_id.into(); let user_id = user_id.into();
let db_pool = self.database.clone();
fn extract_id(s: &str) -> Option<u32> { fn extract_id(s: &str) -> Option<u32> {
if s.len() > 3 && s.to_lowercase().starts_with("id:") { if s.len() > 3 && s.to_lowercase().starts_with("id:") {
@ -134,7 +74,7 @@ impl Sound {
if let Some(id) = extract_id(&query) { if let Some(id) = extract_id(&query) {
let sound = sqlx::query_as_unchecked!( let sound = sqlx::query_as_unchecked!(
Self, Sound,
" "
SELECT name, id, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
@ -158,7 +98,7 @@ SELECT name, id, public, server_id, uploader_id
if strict { if strict {
sound = sqlx::query_as_unchecked!( sound = sqlx::query_as_unchecked!(
Self, Sound,
" "
SELECT name, id, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
@ -179,7 +119,7 @@ SELECT name, id, public, server_id, uploader_id
.await?; .await?;
} else { } else {
sound = sqlx::query_as_unchecked!( sound = sqlx::query_as_unchecked!(
Self, Sound,
" "
SELECT name, id, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
@ -204,6 +144,70 @@ SELECT name, id, public, server_id, uploader_id
} }
} }
async fn autocomplete_user_sounds<U: Into<u64> + Send, G: Into<u64> + Send>(
&self,
query: &str,
user_id: U,
guild_id: G,
) -> Result<Vec<Sound>, 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 (uploader_id = ? OR server_id = ?)
LIMIT 25
",
query,
user_id.into(),
guild_id.into(),
)
.fetch_all(&db_pool)
.await
}
async fn user_sounds<U: Into<u64> + Send>(
&self,
user_id: U,
) -> Result<Vec<Sound>, sqlx::Error> {
let sounds = sqlx::query_as_unchecked!(
Sound,
"
SELECT name, id, public, server_id, uploader_id
FROM sounds
WHERE uploader_id = ?
",
user_id.into()
)
.fetch_all(&self.database)
.await?;
Ok(sounds)
}
async fn guild_sounds<G: Into<u64> + Send>(
&self,
guild_id: G,
) -> Result<Vec<Sound>, sqlx::Error> {
let sounds = sqlx::query_as_unchecked!(
Sound,
"
SELECT name, id, public, server_id, uploader_id
FROM sounds
WHERE server_id = ?
",
guild_id.into()
)
.fetch_all(&self.database)
.await?;
Ok(sounds)
}
}
impl Sound {
async fn src(&self, db_pool: MySqlPool) -> Vec<u8> { async fn src(&self, db_pool: MySqlPool) -> Vec<u8> {
struct Src { struct Src {
src: Vec<u8>, src: Vec<u8>,
@ -229,7 +233,7 @@ SELECT src
pub async fn store_sound_source( pub async fn store_sound_source(
&self, &self,
db_pool: MySqlPool, db_pool: MySqlPool,
) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let caching_location = env::var("CACHING_LOCATION").unwrap_or(String::from("/tmp")); let caching_location = env::var("CACHING_LOCATION").unwrap_or(String::from("/tmp"));
let path_name = format!("{}/sound-{}", caching_location, self.id); let path_name = format!("{}/sound-{}", caching_location, self.id);
@ -241,6 +245,15 @@ SELECT src
file.write_all(&self.src(db_pool).await).await?; file.write_all(&self.src(db_pool).await).await?;
} }
Ok(path_name)
}
pub async fn playable(
&self,
db_pool: MySqlPool,
) -> Result<Restartable, Box<dyn std::error::Error + Send + Sync>> {
let path_name = self.store_sound_source(db_pool).await?;
Ok(Restartable::ffmpeg(path_name, false) Ok(Restartable::ffmpeg(path_name, false)
.await .await
.expect("FFMPEG ERROR!")) .expect("FFMPEG ERROR!"))
@ -397,42 +410,4 @@ INSERT INTO sounds (name, server_id, uploader_id, public, src)
None => Err(Box::new(ErrorTypes::InvalidFile)), None => Err(Box::new(ErrorTypes::InvalidFile)),
} }
} }
pub async fn user_sounds<U: Into<u64>>(
user_id: U,
db_pool: MySqlPool,
) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> {
let sounds = sqlx::query_as_unchecked!(
Sound,
"
SELECT name, id, public, server_id, uploader_id
FROM sounds
WHERE uploader_id = ?
",
user_id.into()
)
.fetch_all(&db_pool)
.await?;
Ok(sounds)
}
pub async fn guild_sounds<G: Into<u64>>(
guild_id: G,
db_pool: MySqlPool,
) -> Result<Vec<Sound>, Box<dyn std::error::Error + Send + Sync>> {
let sounds = sqlx::query_as_unchecked!(
Sound,
"
SELECT name, id, public, server_id, uploader_id
FROM sounds
WHERE server_id = ?
",
guild_id.into()
)
.fetch_all(&db_pool)
.await?;
Ok(sounds)
}
} }

141
src/utils.rs Normal file
View File

@ -0,0 +1,141 @@
use std::sync::Arc;
use poise::serenity::model::{
channel::Channel,
guild::Guild,
id::{ChannelId, UserId},
};
use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call};
use sqlx::MySqlPool;
use tokio::sync::{Mutex, MutexGuard};
use crate::{
models::{
guild_data::CtxGuildData,
sound::{Sound, SoundCtx},
},
Data,
};
pub async fn play_audio(
sound: &mut Sound,
volume: u8,
call_handler: &mut MutexGuard<'_, Call>,
mysql_pool: MySqlPool,
loop_: bool,
) -> Result<TrackHandle, Box<dyn std::error::Error + Send + Sync>> {
let (track, track_handler) = create_player(sound.playable(mysql_pool.clone()).await?.into());
let _ = track_handler.set_volume(volume as f32 / 100.0);
if loop_ {
let _ = track_handler.enable_loop();
} else {
let _ = track_handler.disable_loop();
}
call_handler.play(track);
Ok(track_handler)
}
pub async fn join_channel(
ctx: &poise::serenity_prelude::Context,
guild: Guild,
channel_id: ChannelId,
) -> (Arc<Mutex<Call>>, JoinResult<()>) {
let songbird = songbird::get(ctx).await.unwrap();
let current_user = ctx.cache.current_user_id();
let current_voice_state = guild
.voice_states
.get(&current_user)
.and_then(|voice_state| voice_state.channel_id);
let (call, res) = if current_voice_state == Some(channel_id) {
let call_opt = songbird.get(guild.id);
if let Some(call) = call_opt {
(call, Ok(()))
} else {
let (call, res) = songbird.join(guild.id, channel_id).await;
(call, res)
}
} else {
let (call, res) = songbird.join(guild.id, channel_id).await;
(call, res)
};
{
// set call to deafen
let _ = call.lock().await.deafen(true).await;
}
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
let _ = channel
.edit_voice_state(&ctx, ctx.cache.current_user(), |v| v.suppress(false))
.await;
}
(call, res)
}
pub async fn play_from_query(
ctx: &poise::serenity_prelude::Context,
data: &Data,
guild: Guild,
user_id: UserId,
query: &str,
loop_: bool,
) -> String {
let guild_id = guild.id;
let channel_to_join = guild
.voice_states
.get(&user_id)
.and_then(|voice_state| voice_state.channel_id);
match channel_to_join {
Some(user_channel) => {
let pool = data.database.clone();
let mut sound_vec = data
.search_for_sound(query, guild_id, user_id, true)
.await
.unwrap();
let sound_res = sound_vec.first_mut();
match sound_res {
Some(sound) => {
{
let (call_handler, _) =
join_channel(ctx, guild.clone(), user_channel).await;
let guild_data = data.guild_data(guild_id).await.unwrap();
let mut lock = call_handler.lock().await;
play_audio(
sound,
guild_data.read().await.volume,
&mut lock,
pool,
loop_,
)
.await
.unwrap();
}
format!("Playing sound {} with ID {}", sound.name, sound.id)
}
None => "Couldn't find sound by term provided".to_string(),
}
}
None => "You are not in a voice chat!".to_string(),
}
}