diff --git a/src/cmds/info.rs b/src/cmds/info.rs index 72bc8a5..2252c2e 100644 --- a/src/cmds/info.rs +++ b/src/cmds/info.rs @@ -1,4 +1,4 @@ -use crate::{Context, Error, THEME_COLOR}; +use crate::{consts::THEME_COLOR, Context, Error}; /// Get additional information about the bot #[poise::command(slash_command, category = "Information")] diff --git a/src/cmds/manage.rs b/src/cmds/manage.rs index 938619b..3eab28d 100644 --- a/src/cmds/manage.rs +++ b/src/cmds/manage.rs @@ -1,8 +1,14 @@ use std::time::Duration; 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 #[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")] pub async fn delete_sound( 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> { let pool = ctx.data().database.clone(); let uid = ctx.author().id.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(); match sound_result { @@ -174,14 +182,16 @@ pub async fn delete_sound( #[poise::command(slash_command, rename = "public", category = "Manage")] pub async fn change_public( 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> { let pool = ctx.data().database.clone(); let uid = ctx.author().id.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(); match sound_result { @@ -210,3 +220,39 @@ pub async fn change_public( 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(()) +} diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index ae5369c..a0cb39c 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -1,6 +1,24 @@ +use crate::{models::sound::SoundCtx, Context}; + pub mod info; pub mod manage; pub mod play; -// pub mod search; -// pub mod settings; -// pub mod stop; +pub mod search; +pub mod settings; +pub mod stop; + +pub async fn autocomplete_sound( + ctx: Context<'_>, + partial: String, +) -> Vec> { + 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() +} diff --git a/src/cmds/play.rs b/src/cmds/play.rs index 0195784..2a7a00f 100644 --- a/src/cmds/play.rs +++ b/src/cmds/play.rs @@ -2,32 +2,58 @@ use poise::serenity::{ 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 #[poise::command(slash_command)] pub async fn play( 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> { let guild = ctx.guild().unwrap(); - ctx.say(play_from_query(&ctx, guild, ctx.author().id, &name, false).await) - .await?; + ctx.say( + play_from_query( + &ctx.discord(), + &ctx.data(), + guild, + ctx.author().id, + &name, + false, + ) + .await, + ) + .await?; Ok(()) } /// Loop a sound in your current voice channel -#[poise::command(slash_command)] +#[poise::command(slash_command, rename = "loop")] pub async fn loop_play( 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> { let guild = ctx.guild().unwrap(); - ctx.say(play_from_query(&ctx, guild, ctx.author().id, &name, true).await) - .await?; + ctx.say( + play_from_query( + &ctx.discord(), + &ctx.data(), + guild, + ctx.author().id, + &name, + true, + ) + .await, + ) + .await?; Ok(()) } @@ -36,36 +62,84 @@ pub async fn loop_play( #[poise::command(slash_command, rename = "soundboard", category = "Play")] pub async fn soundboard( ctx: Context<'_>, - #[description = "Name or ID of sound for button 1"] sound_1: String, - #[description = "Name or ID of sound for button 2"] sound_2: Option, - #[description = "Name or ID of sound for button 3"] sound_3: Option, - #[description = "Name or ID of sound for button 4"] sound_4: Option, - #[description = "Name or ID of sound for button 5"] sound_5: Option, - #[description = "Name or ID of sound for button 6"] sound_6: Option, - #[description = "Name or ID of sound for button 7"] sound_7: Option, - #[description = "Name or ID of sound for button 8"] sound_8: Option, - #[description = "Name or ID of sound for button 9"] sound_9: Option, - #[description = "Name or ID of sound for button 10"] sound_10: Option, - #[description = "Name or ID of sound for button 11"] sound_11: Option, - #[description = "Name or ID of sound for button 12"] sound_12: Option, - #[description = "Name or ID of sound for button 13"] sound_13: Option, - #[description = "Name or ID of sound for button 14"] sound_14: Option, - #[description = "Name or ID of sound for button 15"] sound_15: Option, - #[description = "Name or ID of sound for button 16"] sound_16: Option, - #[description = "Name or ID of sound for button 17"] sound_17: Option, - #[description = "Name or ID of sound for button 18"] sound_18: Option, - #[description = "Name or ID of sound for button 19"] sound_19: Option, - #[description = "Name or ID of sound for button 20"] sound_20: Option, - #[description = "Name or ID of sound for button 21"] sound_21: Option, - #[description = "Name or ID of sound for button 22"] sound_22: Option, - #[description = "Name or ID of sound for button 23"] sound_23: Option, - #[description = "Name or ID of sound for button 24"] sound_24: Option, - #[description = "Name or ID of sound for button 25"] sound_25: Option, + #[description = "Name or ID of sound for button 1"] + #[autocomplete = "autocomplete_sound"] + sound_1: String, + #[description = "Name or ID of sound for button 2"] + #[autocomplete = "autocomplete_sound"] + sound_2: Option, + #[description = "Name or ID of sound for button 3"] + #[autocomplete = "autocomplete_sound"] + sound_3: Option, + #[description = "Name or ID of sound for button 4"] + #[autocomplete = "autocomplete_sound"] + sound_4: Option, + #[description = "Name or ID of sound for button 5"] + #[autocomplete = "autocomplete_sound"] + sound_5: Option, + #[description = "Name or ID of sound for button 6"] + #[autocomplete = "autocomplete_sound"] + sound_6: Option, + #[description = "Name or ID of sound for button 7"] + #[autocomplete = "autocomplete_sound"] + sound_7: Option, + #[description = "Name or ID of sound for button 8"] + #[autocomplete = "autocomplete_sound"] + sound_8: Option, + #[description = "Name or ID of sound for button 9"] + #[autocomplete = "autocomplete_sound"] + sound_9: Option, + #[description = "Name or ID of sound for button 10"] + #[autocomplete = "autocomplete_sound"] + sound_10: Option, + #[description = "Name or ID of sound for button 11"] + #[autocomplete = "autocomplete_sound"] + sound_11: Option, + #[description = "Name or ID of sound for button 12"] + #[autocomplete = "autocomplete_sound"] + sound_12: Option, + #[description = "Name or ID of sound for button 13"] + #[autocomplete = "autocomplete_sound"] + sound_13: Option, + #[description = "Name or ID of sound for button 14"] + #[autocomplete = "autocomplete_sound"] + sound_14: Option, + #[description = "Name or ID of sound for button 15"] + #[autocomplete = "autocomplete_sound"] + sound_15: Option, + #[description = "Name or ID of sound for button 16"] + #[autocomplete = "autocomplete_sound"] + sound_16: Option, + #[description = "Name or ID of sound for button 17"] + #[autocomplete = "autocomplete_sound"] + sound_17: Option, + #[description = "Name or ID of sound for button 18"] + #[autocomplete = "autocomplete_sound"] + sound_18: Option, + #[description = "Name or ID of sound for button 19"] + #[autocomplete = "autocomplete_sound"] + sound_19: Option, + #[description = "Name or ID of sound for button 20"] + #[autocomplete = "autocomplete_sound"] + sound_20: Option, + #[description = "Name or ID of sound for button 21"] + #[autocomplete = "autocomplete_sound"] + sound_21: Option, + #[description = "Name or ID of sound for button 22"] + #[autocomplete = "autocomplete_sound"] + sound_22: Option, + #[description = "Name or ID of sound for button 23"] + #[autocomplete = "autocomplete_sound"] + sound_23: Option, + #[description = "Name or ID of sound for button 24"] + #[autocomplete = "autocomplete_sound"] + sound_24: Option, + #[description = "Name or ID of sound for button 25"] + #[autocomplete = "autocomplete_sound"] + sound_25: Option, ) -> Result<(), Error> { ctx.defer().await?; - let pool = ctx.data().database.clone(); - let query_terms = [ Some(sound_1), sound_2, @@ -97,14 +171,10 @@ pub async fn soundboard( let mut sounds = vec![]; for sound in query_terms.iter().flatten() { - let search = Sound::search_for_sound( - &sound, - ctx.guild_id().unwrap(), - ctx.author().id, - pool.clone(), - true, - ) - .await?; + let search = ctx + .data() + .search_for_sound(&sound, ctx.guild_id().unwrap(), ctx.author().id, true) + .await?; if let Some(sound) = search.first() { if !sounds.contains(sound) { diff --git a/src/cmds/search.rs b/src/cmds/search.rs index 4e3a285..0cc3b06 100644 --- a/src/cmds/search.rs +++ b/src/cmds/search.rs @@ -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) -> CreateReply<'a> { + let mut builder = CreateReply::default(); -fn format_search_results(search_results: Vec) -> CreateGenericResponse { let mut current_character_count = 0; let title = "Public sounds matching filter:"; @@ -11,49 +18,25 @@ fn format_search_results(search_results: Vec) -> CreateGenericResponse { .filter(|item| { 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")] -#[group("Search")] -#[description("Show the sounds uploaded by you or to your server")] -#[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::() - .cloned() - .expect("Could not get SQLPool from data"); - +/// Show the sounds uploaded to this server +#[poise::command(slash_command, rename = "list")] +pub async fn list_sounds(ctx: Context<'_>) -> Result<(), Error> { let sounds; let mut message_buffer; - if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) { - sounds = Sound::user_sounds(invoke.author_id(), pool).await?; + sounds = ctx.data().guild_sounds(ctx.guild_id().unwrap()).await?; - message_buffer = "All your sounds: ".to_string(); - } else { - sounds = Sound::guild_sounds(invoke.guild_id().unwrap(), pool).await?; - - message_buffer = "All sounds on this server: ".to_string(); - } + message_buffer = "Sounds on this server: ".to_string(); + // todo change this to iterator for sound in sounds { message_buffer.push_str( format!( @@ -65,85 +48,77 @@ pub async fn list_sounds( ); if message_buffer.len() > 2000 { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(message_buffer), - ) - .await?; + ctx.say(message_buffer).await?; message_buffer = "".to_string(); } } if message_buffer.len() > 0 { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(message_buffer), - ) - .await?; + ctx.say(message_buffer).await?; } Ok(()) } -#[command("search")] -#[group("Search")] -#[description("Search for sounds")] -#[arg( - name = "query", - 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::() - .cloned() - .expect("Could not get SQLPool from data"); +/// Show all sounds you have uploaded +#[poise::command(slash_command, rename = "me")] +pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> { + let sounds; + let mut message_buffer; - let query = args.named("query").unwrap(); + sounds = ctx.data().user_sounds(ctx.author().id).await?; - let search_results = Sound::search_for_sound( - query, - invoke.guild_id().unwrap(), - invoke.author_id(), - pool, - false, - ) - .await?; + message_buffer = "Sounds on this server: ".to_string(); - invoke - .respond(ctx.http.clone(), format_search_results(search_results)) - .await?; + // todo change this to iterator + for sound in sounds { + message_buffer.push_str( + 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(()) } -#[command("random")] -#[group("Search")] -#[description("Show a page of random sounds")] -pub async fn show_random_sounds( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - _args: Args, -) -> CommandResult { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); +/// 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?; + ctx.send(|m| { + *m = format_search_results(search_results); + m + }) + .await?; + + Ok(()) +} + +/// Show a page of random sounds +#[poise::command(slash_command, rename = "random")] +pub async fn show_random_sounds(ctx: Context<'_>) -> Result<(), Error> { let search_results = sqlx::query_as_unchecked!( Sound, " @@ -154,13 +129,14 @@ SELECT name, id, public, server_id, uploader_id LIMIT 25 " ) - .fetch_all(&pool) - .await - .unwrap(); + .fetch_all(&ctx.data().database) + .await?; - invoke - .respond(ctx.http.clone(), format_search_results(search_results)) - .await?; + ctx.send(|m| { + *m = format_search_results(search_results); + m + }) + .await?; Ok(()) } diff --git a/src/cmds/settings.rs b/src/cmds/settings.rs index 9e15556..c8a8676 100644 --- a/src/cmds/settings.rs +++ b/src/cmds/settings.rs @@ -1,307 +1,126 @@ -use regex_command_attr::command; -use serenity::{client::Context, framework::standard::CommandResult}; - use crate::{ - framework::{Args, CommandInvoke, CreateGenericResponse}, - guild_data::CtxGuildData, - sound::{JoinSoundCtx, Sound}, - MySQL, + models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::SoundCtx}, + Context, Error, }; -#[command("volume")] -#[aliases("vol")] -#[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%")] +/// Change the bot's volume in this server +#[poise::command(slash_command, rename = "volume")] pub async fn change_volume( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); - - let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await; + ctx: Context<'_>, + #[description = "New volume as a percentage"] volume: Option, +) -> Result<(), Error> { + let guild_data_opt = ctx.guild_data(ctx.guild_id().unwrap()).await; let guild_data = guild_data_opt.unwrap(); - if let Some(volume) = args.named("volume").map(|i| i.parse::().ok()).flatten() { - guild_data.write().await.volume = volume; + if let Some(volume) = volume { + guild_data.write().await.volume = volume as u8; - guild_data.read().await.commit(pool).await?; - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(format!("Volume changed to {}%", volume)), - ) + guild_data + .read() + .await + .commit(ctx.data().database.clone()) .await?; + + ctx.say(format!("Volume changed to {}%", volume)).await?; } else { let read = guild_data.read().await; - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(format!( - "Current server volume: {vol}%. Change the volume with `/volume `", - vol = read.volume - )), - ) - .await?; - } - - Ok(()) -} - -#[command("prefix")] -#[required_permissions(Restricted)] -#[kind(Text)] -#[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::() - .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 `", - prefix = guild_data.read().await.prefix - )), - ) - .await?; - } - - Ok(()) -} - -#[command("roles")] -#[required_permissions(Restricted)] -#[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::() - .cloned() - .expect("Could not get SQLPool from data"); - - let role_id = args.named("role").unwrap().parse::().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)), - ) + ctx.say(format!( + "Current server volume: {vol}%. Change the volume with `/volume `", + vol = read.volume + )) .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")] +/// Manage greet sounds on this server +#[poise::command(slash_command, rename = "greet")] +pub async fn greet_sound(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// Set a join sound +#[poise::command(slash_command, rename = "set")] pub async fn set_greet_sound( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - let pool = ctx - .data - .read() - .await - .get::() - .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, - ) + ctx: Context<'_>, + #[description = "Name or ID of sound to set as your join sound"] name: String, +) -> Result<(), Error> { + let sound_vec = ctx + .data() + .search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true) .await?; - match sound_vec.first() { - Some(sound) => { - ctx.update_join_sound(user_id, Some(sound.id)).await; + match sound_vec.first() { + Some(sound) => { + ctx.data() + .update_join_sound(ctx.author().id, Some(sound.id)) + .await; - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(format!( - "Greet sound has been set to {} (ID {})", - sound.name, sound.id - )), - ) - .await?; - } + ctx.say(format!( + "Greet sound has been set to {} (ID {})", + sound.name, sound.id + )) + .await?; + } - None => { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Could not find a sound by that name."), - ) - .await?; - } + None => { + ctx.say("Could not find a sound by that name.").await?; } } Ok(()) } -#[command("allow_greet")] -#[group("Settings")] -#[description("Configure whether users should be able to use join sounds")] -#[required_permissions(Restricted)] -#[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::() - .cloned() - .expect("Could not acquire SQL pool from data"); +/// Set a join sound +#[poise::command(slash_command, rename = "unset")] +pub async fn unset_greet_sound(ctx: Context<'_>) -> Result<(), Error> { + ctx.data().update_join_sound(ctx.author().id, None).await; - 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 { - let current = guild_data.read().await.allow_greets; + guild_data.write().await.allow_greets = false; - { - guild_data.write().await.allow_greets = !current; - } - - 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" } - )), - ) + guild_data + .read() + .await + .commit(ctx.data().database.clone()) .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(()) } diff --git a/src/cmds/stop.rs b/src/cmds/stop.rs index dccc318..9a26a5b 100644 --- a/src/cmds/stop.rs +++ b/src/cmds/stop.rs @@ -1,22 +1,12 @@ -use regex_command_attr::command; -use serenity::{client::Context, framework::standard::CommandResult}; use songbird; -use crate::framework::{Args, CommandInvoke, CreateGenericResponse}; +use crate::{Context, Error}; -#[command("stop")] -#[required_permissions(Managed)] -#[group("Stop")] -#[description("Stop the bot from playing")] -pub async fn stop_playing( - 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); +/// Stop the bot from playing +#[poise::command(slash_command, rename = "stop")] +pub async fn stop_playing(ctx: Context<'_>) -> Result<(), Error> { + let songbird = songbird::get(ctx.discord()).await.unwrap(); + let call_opt = songbird.get(ctx.guild_id().unwrap()); if let Some(call) = call_opt { let mut lock = call.lock().await; @@ -24,31 +14,18 @@ pub async fn stop_playing( lock.stop(); } - invoke - .respond(ctx.http.clone(), CreateGenericResponse::new().content("👍")) - .await?; + ctx.say("👍").await?; Ok(()) } -#[command] -#[aliases("dc")] -#[required_permissions(Managed)] -#[group("Stop")] -#[description("Disconnect the bot")] -pub async fn disconnect( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - _args: Args, -) -> CommandResult { - let guild_id = invoke.guild_id().unwrap(); +/// Disconnect the bot +#[poise::command(slash_command)] +pub async fn disconnect(ctx: Context<'_>) -> Result<(), Error> { + let songbird = songbird::get(ctx.discord()).await.unwrap(); + let _ = songbird.leave(ctx.guild_id().unwrap()).await; - let songbird = songbird::get(ctx).await.unwrap(); - let _ = songbird.leave(guild_id).await; - - invoke - .respond(ctx.http.clone(), CreateGenericResponse::new().content("👍")) - .await?; + ctx.say("👍").await?; Ok(()) } diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..5b7e8fe --- /dev/null +++ b/src/consts.rs @@ -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::().unwrap(); + pub static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::().unwrap(); + pub static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::().unwrap(); +} diff --git a/src/event_handlers.rs b/src/event_handlers.rs index 42b5c08..8ffa91d 100644 --- a/src/event_handlers.rs +++ b/src/event_handlers.rs @@ -1,27 +1,19 @@ use std::{collections::HashMap, env}; -use poise::serenity::{async_trait, model::channel::Channel, prelude::Context, utils::shard_id}; -use songbird::{Event, EventContext, EventHandler as SongbirdEventHandler}; - -use crate::{ - guild_data::CtxGuildData, - join_channel, play_audio, - sound::{JoinSoundCtx, Sound}, - Data, Error, +use poise::serenity::{ + model::{ + channel::Channel, + interactions::{Interaction, InteractionResponseType}, + }, + prelude::Context, + utils::shard_id, }; -pub struct RestartTrack; - -#[async_trait] -impl SongbirdEventHandler for RestartTrack { - async fn act(&self, ctx: &EventContext<'_>) -> Option { - if let EventContext::Track(&[(_state, track)]) = ctx { - let _ = track.seek_time(Default::default()); - } - - None - } -} +use crate::{ + models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::Sound}, + utils::{join_channel, play_audio, play_from_query}, + Data, Error, +}; pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> { 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(); + } + } + _ => {} + }, _ => {} } diff --git a/src/main.rs b/src/main.rs index 079671a..89d629b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,11 @@ extern crate lazy_static; mod cmds; +mod consts; mod error; mod event_handlers; -mod guild_data; -mod sound; +mod models; +mod utils; use std::{env, sync::Arc}; @@ -14,21 +15,15 @@ use dotenv::dotenv; use poise::serenity::{ builder::CreateApplicationCommands, model::{ - channel::Channel, gateway::{Activity, GatewayIntents}, - guild::Guild, - id::{ChannelId, GuildId, UserId}, + id::{GuildId, UserId}, }, }; -use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call, SerenityInit}; +use songbird::SerenityInit; use sqlx::mysql::MySqlPool; -use tokio::sync::{Mutex, MutexGuard, RwLock}; +use tokio::sync::RwLock; -use crate::{ - event_handlers::listener, - guild_data::{CtxGuildData, GuildData}, - sound::Sound, -}; +use crate::{event_handlers::listener, models::guild_data::GuildData}; pub struct Data { database: MySqlPool, @@ -40,137 +35,6 @@ pub struct Data { type Error = Box; 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::().unwrap(); - static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::().unwrap(); - static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::().unwrap(); -} - -async fn play_audio( - sound: &mut Sound, - volume: u8, - call_handler: &mut MutexGuard<'_, Call>, - mysql_pool: MySqlPool, - loop_: bool, -) -> Result> { - 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>, JoinResult<()>) { - let songbird = songbird::get(ctx).await.unwrap(); - let current_user = ctx.cache.current_user_id(); - - let current_voice_state = guild - .voice_states - .get(¤t_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( ctx: &poise::serenity::client::Context, framework: &poise::Framework, @@ -215,10 +79,28 @@ async fn main() -> Result<(), Box> { cmds::info::info(), cmds::manage::change_public(), cmds::manage::upload_new_sound(), + cmds::manage::download_file(), cmds::manage::delete_sound(), cmds::play::play(), cmds::play::loop_play(), 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, listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)), diff --git a/src/guild_data.rs b/src/models/guild_data.rs similarity index 100% rename from src/guild_data.rs rename to src/models/guild_data.rs diff --git a/src/models/join_sound.rs b/src/models/join_sound.rs new file mode 100644 index 0000000..b6c8cd4 --- /dev/null +++ b/src/models/join_sound.rs @@ -0,0 +1,87 @@ +use poise::serenity::{async_trait, model::id::UserId}; + +use crate::Data; + +#[async_trait] +pub trait JoinSoundCtx { + async fn join_sound + Send + Sync>(&self, user_id: U) -> Option; + async fn update_join_sound + Send + Sync>( + &self, + user_id: U, + join_id: Option, + ); +} + +#[async_trait] +impl JoinSoundCtx for Data { + async fn join_sound + Send + Sync>(&self, user_id: U) -> Option { + 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 + Send + Sync>( + &self, + user_id: U, + join_id: Option, + ) { + 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; + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..262eedf --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,3 @@ +pub mod guild_data; +pub mod join_sound; +pub mod sound; diff --git a/src/sound.rs b/src/models/sound.rs similarity index 77% rename from src/sound.rs rename to src/models/sound.rs index b2041db..86784b6 100644 --- a/src/sound.rs +++ b/src/models/sound.rs @@ -1,96 +1,11 @@ use std::{env, path::Path}; -use poise::serenity::{async_trait, model::id::UserId}; +use poise::serenity::async_trait; use songbird::input::restartable::Restartable; -use sqlx::mysql::MySqlPool; +use sqlx::{mysql::MySqlPool, Error}; use tokio::{fs::File, io::AsyncWriteExt, process::Command}; -use super::error::ErrorTypes; -use crate::Data; - -#[async_trait] -pub trait JoinSoundCtx { - async fn join_sound + Send + Sync>(&self, user_id: U) -> Option; - async fn update_join_sound + Send + Sync>( - &self, - user_id: U, - join_id: Option, - ); -} - -#[async_trait] -impl JoinSoundCtx for Data { - async fn join_sound + Send + Sync>(&self, user_id: U) -> Option { - 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 + Send + Sync>( - &self, - user_id: U, - join_id: Option, - ) { - 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; - } -} +use crate::{error::ErrorTypes, Data}; #[derive(Clone)] pub struct Sound { @@ -107,16 +22,41 @@ impl PartialEq for Sound { } } -impl Sound { - pub async fn search_for_sound, U: Into>( +#[async_trait] +pub trait SoundCtx { + async fn search_for_sound + Send, U: Into + Send>( + &self, + query: &str, + guild_id: G, + user_id: U, + strict: bool, + ) -> Result, sqlx::Error>; + async fn autocomplete_user_sounds + Send, G: Into + Send>( + &self, + query: &str, + user_id: U, + guild_id: G, + ) -> Result, sqlx::Error>; + async fn user_sounds + Send>(&self, user_id: U) + -> Result, sqlx::Error>; + async fn guild_sounds + Send>( + &self, + guild_id: G, + ) -> Result, sqlx::Error>; +} + +#[async_trait] +impl SoundCtx for Data { + async fn search_for_sound + Send, U: Into + Send>( + &self, query: &str, guild_id: G, user_id: U, - db_pool: MySqlPool, strict: bool, ) -> Result, sqlx::Error> { let guild_id = guild_id.into(); let user_id = user_id.into(); + let db_pool = self.database.clone(); fn extract_id(s: &str) -> Option { if s.len() > 3 && s.to_lowercase().starts_with("id:") { @@ -134,7 +74,7 @@ impl Sound { if let Some(id) = extract_id(&query) { let sound = sqlx::query_as_unchecked!( - Self, + Sound, " SELECT name, id, public, server_id, uploader_id FROM sounds @@ -158,7 +98,7 @@ SELECT name, id, public, server_id, uploader_id if strict { sound = sqlx::query_as_unchecked!( - Self, + Sound, " SELECT name, id, public, server_id, uploader_id FROM sounds @@ -179,7 +119,7 @@ SELECT name, id, public, server_id, uploader_id .await?; } else { sound = sqlx::query_as_unchecked!( - Self, + Sound, " SELECT name, id, public, server_id, uploader_id FROM sounds @@ -204,6 +144,70 @@ SELECT name, id, public, server_id, uploader_id } } + async fn autocomplete_user_sounds + Send, G: Into + Send>( + &self, + query: &str, + user_id: U, + guild_id: G, + ) -> Result, 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 + Send>( + &self, + user_id: U, + ) -> Result, 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 + Send>( + &self, + guild_id: G, + ) -> Result, 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 { struct Src { src: Vec, @@ -229,7 +233,7 @@ SELECT src pub async fn store_sound_source( &self, db_pool: MySqlPool, - ) -> Result> { + ) -> Result> { let caching_location = env::var("CACHING_LOCATION").unwrap_or(String::from("/tmp")); let path_name = format!("{}/sound-{}", caching_location, self.id); @@ -241,6 +245,15 @@ SELECT src file.write_all(&self.src(db_pool).await).await?; } + Ok(path_name) + } + + pub async fn playable( + &self, + db_pool: MySqlPool, + ) -> Result> { + let path_name = self.store_sound_source(db_pool).await?; + Ok(Restartable::ffmpeg(path_name, false) .await .expect("FFMPEG ERROR!")) @@ -397,42 +410,4 @@ INSERT INTO sounds (name, server_id, uploader_id, public, src) None => Err(Box::new(ErrorTypes::InvalidFile)), } } - - pub async fn user_sounds>( - user_id: U, - db_pool: MySqlPool, - ) -> Result, Box> { - 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>( - guild_id: G, - db_pool: MySqlPool, - ) -> Result, Box> { - 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) - } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ed5ac71 --- /dev/null +++ b/src/utils.rs @@ -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> { + 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>, JoinResult<()>) { + let songbird = songbird::get(ctx).await.unwrap(); + let current_user = ctx.cache.current_user_id(); + + let current_voice_state = guild + .voice_states + .get(¤t_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(), + } +}