diff --git a/regex_command_attr/src/structures.rs b/regex_command_attr/src/structures.rs index 9ca087f..36e9331 100644 --- a/regex_command_attr/src/structures.rs +++ b/regex_command_attr/src/structures.rs @@ -7,7 +7,7 @@ use syn::{ Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility, }; -use crate::util::{self, Argument, AsOption, Parenthesised}; +use crate::util::{self, Argument, Parenthesised}; fn parse_argument(arg: FnArg) -> Result { match arg { @@ -290,7 +290,7 @@ impl Default for Arg { pub(crate) struct Options { pub aliases: Vec, pub description: String, - pub group: AsOption, + pub group: String, pub examples: Vec, pub required_permissions: PermissionLevel, pub allow_slash: bool, @@ -302,6 +302,7 @@ impl Options { pub fn new() -> Self { Self { allow_slash: true, + group: "Other".to_string(), ..Default::default() } } diff --git a/src/cmds/info.rs b/src/cmds/info.rs new file mode 100644 index 0000000..7b82717 --- /dev/null +++ b/src/cmds/info.rs @@ -0,0 +1,198 @@ +use regex_command_attr::command; + +use serenity::{client::Context, framework::standard::CommandResult}; + +use crate::{ + framework::{Args, CommandInvoke, CreateGenericResponse, RegexFramework}, + THEME_COLOR, +}; + +use std::{collections::HashMap, sync::Arc}; + +#[command] +#[group("Information")] +#[description("Get information on the commands of the bot")] +#[arg( + name = "command", + description = "Get help for a specific command", + kind = "String", + required = false +)] +#[example("`/help` - see all commands")] +#[example("`/help play` - get help about the `play` command")] +pub async fn help( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + args: Args, +) -> CommandResult { + fn get_groups(framework: Arc) -> HashMap<&'static str, Vec<&'static str>> { + let mut groups = HashMap::new(); + + for command in &framework.commands_ { + let entry = groups.entry(command.group).or_insert(vec![]); + + entry.push(command.names[0]); + } + + groups + } + + let framework = ctx + .data + .read() + .await + .get::() + .cloned() + .unwrap(); + + if let Some(command_name) = args.named("command") { + if let Some(command) = framework.commands.get(command_name) { + let examples = if command.examples.is_empty() { + "".to_string() + } else { + format!( + "**Examples** +{}", + command + .examples + .iter() + .map(|e| format!(" • {}", e)) + .collect::>() + .join("\n") + ) + }; + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { + e.title(format!("{} Help", command_name)) + .color(THEME_COLOR) + .description(format!( + "**Aliases** +{} + +**Overview** + • {} +**Arguments** +{} + +{}", + command + .names + .iter() + .map(|n| format!("`{}`", n)) + .collect::>() + .join(" "), + command.desc, + command + .args + .iter() + .map(|a| format!( + " • `{}` {} - {}", + a.name, + if a.required { "" } else { "[optional]" }, + a.description + )) + .collect::>() + .join("\n"), + examples + )) + }), + ) + .await?; + } else { + let groups = get_groups(framework); + let groups_iter = groups.iter().map(|(name, commands)| { + ( + name, + commands + .iter() + .map(|c| format!("`{}`", c)) + .collect::>() + .join(" "), + true, + ) + }); + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { + e.title("Invalid Command") + .color(THEME_COLOR) + .description("Type `/help command` to view help about a command below:") + .fields(groups_iter) + }), + ) + .await?; + } + } else { + let groups = get_groups(framework); + let groups_iter = groups.iter().map(|(name, commands)| { + ( + name, + commands + .iter() + .map(|c| format!("`{}`", c)) + .collect::>() + .join(" "), + true, + ) + }); + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { + e.title("Help") + .color(THEME_COLOR) + .description("Type `/help command` to view help about a command below:") + .fields(groups_iter) + }), + ) + .await?; + } + + Ok(()) +} + +#[command] +#[group("Information")] +#[aliases("invite")] +#[description("Get additional information on the bot")] +async fn info( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + _args: Args, +) -> CommandResult { + let current_user = ctx.cache.current_user().await; + + invoke.respond(ctx.http.clone(), CreateGenericResponse::new() + .embed(|e| e + .title("Info") + .color(THEME_COLOR) + .footer(|f| f + .text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION")))) + .description(format!("Default prefix: `?` + +Reset prefix: `@{0} prefix ?` + +Invite me: https://discord.com/api/oauth2/authorize?client_id={1}&permissions=3165184&scope=applications.commands%20bot + +**Welcome to SoundFX!** +Developer: <@203532103185465344> +Find me on https://discord.jellywx.com/ and on https://github.com/JellyWX :) + +**Sound Credits** +\"The rain falls against the parasol\" https://freesound.org/people/straget/ +\"Heavy Rain\" https://freesound.org/people/lebaston100/ +\"Rain on Windows, Interior, A\" https://freesound.org/people/InspectorJ/ +\"Seaside Waves, Close, A\" https://freesound.org/people/InspectorJ/ +\"Small River 1 - Fast - Close\" https://freesound.org/people/Pfannkuchn/ + +**An online dashboard is available!** Visit https://soundfx.jellywx.com/dashboard +There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**", current_user.name, current_user.id.as_u64())))).await?; + + Ok(()) +} diff --git a/src/cmds/manage.rs b/src/cmds/manage.rs new file mode 100644 index 0000000..d22eb24 --- /dev/null +++ b/src/cmds/manage.rs @@ -0,0 +1,343 @@ +use regex_command_attr::command; + +use serenity::{ + client::Context, + framework::standard::CommandResult, + model::id::{GuildId, RoleId}, +}; + +use crate::{ + framework::{Args, CommandInvoke, CreateGenericResponse}, + sound::Sound, + MySQL, MAX_SOUNDS, PATREON_GUILD, PATREON_ROLE, +}; + +use std::time::Duration; + +#[command("upload")] +#[group("Manage")] +#[description("Upload a new sound to the bot")] +#[arg( + name = "name", + description = "Name to upload sound to", + kind = "String", + required = true +)] +pub async fn upload_new_sound( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + args: Args, +) -> CommandResult { + fn is_numeric(s: &String) -> bool { + for char in s.chars() { + if char.is_digit(10) { + continue; + } else { + return false; + } + } + true + } + + let new_name = args + .named("name") + .map(|n| n.to_string()) + .unwrap_or(String::new()); + + if !new_name.is_empty() && new_name.len() <= 20 { + if !is_numeric(&new_name) { + let pool = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + // need to check the name is not currently in use by the user + let count_name = + Sound::count_named_user_sounds(invoke.author_id().0, &new_name, pool.clone()) + .await?; + if count_name > 0 { + invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("You are already using that name. Please choose a unique name for your upload.")).await?; + } else { + // need to check how many sounds user currently has + let count = Sound::count_user_sounds(invoke.author_id().0, pool.clone()).await?; + let mut permit_upload = true; + + // need to check if user is patreon or nah + if count >= *MAX_SOUNDS { + let patreon_guild_member = GuildId(*PATREON_GUILD) + .member(ctx, invoke.author_id()) + .await; + + if let Ok(member) = patreon_guild_member { + permit_upload = member.roles.contains(&RoleId(*PATREON_ROLE)); + } else { + permit_upload = false; + } + } + + if permit_upload { + let attachment = if let Some(attachment) = invoke + .msg() + .map(|m| m.attachments.get(0).map(|a| a.url.clone())) + .flatten() + { + Some(attachment) + } else { + invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Please now upload an audio file under 1MB in size (larger files will be automatically trimmed):")).await?; + + let reply = invoke + .channel_id() + .await_reply(&ctx) + .author_id(invoke.author_id()) + .timeout(Duration::from_secs(120)) + .await; + + match reply { + Some(reply_msg) => { + if let Some(attachment) = reply_msg.attachments.get(0) { + Some(attachment.url.clone()) + } else { + invoke.followup(ctx.http.clone(), CreateGenericResponse::new().content("Please upload 1 attachment following your upload command. Aborted")).await?; + + None + } + } + + None => { + invoke + .followup( + ctx.http.clone(), + CreateGenericResponse::new() + .content("Upload timed out. Please redo the command"), + ) + .await?; + + None + } + } + }; + + if let Some(url) = attachment { + match Sound::create_anon( + &new_name, + url.as_str(), + invoke.guild_id().unwrap().0, + invoke.author_id().0, + pool, + ) + .await + { + Ok(_) => { + invoke + .followup( + ctx.http.clone(), + CreateGenericResponse::new() + .content("Sound has been uploaded"), + ) + .await?; + } + + Err(e) => { + println!("Error occurred during upload: {:?}", e); + invoke + .followup( + ctx.http.clone(), + CreateGenericResponse::new() + .content("Sound failed to upload."), + ) + .await?; + } + } + } + } else { + invoke.respond( + ctx.http.clone(), + CreateGenericResponse::new().content(format!( + "You have reached the maximum number of sounds ({}). Either delete some with `?delete` or join our Patreon for unlimited uploads at **https://patreon.com/jellywx**", + *MAX_SOUNDS, + ))).await?; + } + } + } else { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content("Please ensure the sound name contains a non-numerical character"), + ) + .await?; + } + } else { + invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Usage: `?upload `. Please ensure the name provided is less than 20 characters in length")).await?; + } + + Ok(()) +} + +#[command("delete")] +#[group("Manage")] +#[description("Delete a sound you have uploaded")] +#[arg( + name = "query", + description = "Delete sound with the specified name or ID", + kind = "String", + required = true +)] +pub async fn delete_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 uid = invoke.author_id().0; + let gid = invoke.guild_id().unwrap().0; + + let name = args + .named("query") + .map(|s| s.to_owned()) + .unwrap_or(String::new()); + + let sound_vec = Sound::search_for_sound(&name, gid, uid, pool.clone(), true).await?; + let sound_result = sound_vec.first(); + + match sound_result { + Some(sound) => { + if sound.uploader_id != Some(uid) && sound.server_id != gid { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content( + "You can only delete sounds from this guild or that you have uploaded.", + ), + ) + .await?; + } else { + let has_perms = { + if let Ok(member) = invoke.member(&ctx).await { + if let Ok(perms) = member.permissions(&ctx).await { + perms.manage_guild() + } else { + false + } + } else { + false + } + }; + + if sound.uploader_id == Some(uid) || has_perms { + sound.delete(pool).await?; + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("Sound has been deleted"), + ) + .await?; + } else { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content( + "Only server admins can delete sounds uploaded by other users.", + ), + ) + .await?; + } + } + } + + None => { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("Sound could not be found by that name."), + ) + .await?; + } + } + + Ok(()) +} + +#[command("public")] +#[group("Manage")] +#[description("Change a sound between public and private")] +#[arg( + name = "query", + kind = "String", + description = "Sound name or ID to change the privacy setting of", + required = true +)] +pub async fn change_public( + 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 uid = invoke.author_id().as_u64().to_owned(); + + let name = args.named("query").unwrap(); + let gid = *invoke.guild_id().unwrap().as_u64(); + + let mut sound_vec = Sound::search_for_sound(name, gid, uid, pool.clone(), true).await?; + let sound_result = sound_vec.first_mut(); + + match sound_result { + Some(sound) => { + if sound.uploader_id != Some(uid) { + invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("You can only change the visibility of sounds you have uploaded. Use `?list me` to view your sounds")).await?; + } else { + if sound.public { + sound.public = false; + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content("Sound has been set to private 🔒"), + ) + .await?; + } else { + sound.public = true; + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("Sound has been set to public 🔓"), + ) + .await?; + } + + sound.commit(pool).await? + } + } + + None => { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("Sound could not be found by that name."), + ) + .await?; + } + } + + Ok(()) +} diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs new file mode 100644 index 0000000..1a8aa9c --- /dev/null +++ b/src/cmds/mod.rs @@ -0,0 +1,6 @@ +pub mod info; +pub mod manage; +pub mod play; +pub mod search; +pub mod settings; +pub mod stop; diff --git a/src/cmds/play.rs b/src/cmds/play.rs new file mode 100644 index 0000000..2151145 --- /dev/null +++ b/src/cmds/play.rs @@ -0,0 +1,405 @@ +use regex_command_attr::command; + +use serenity::{ + builder::CreateActionRow, + client::Context, + framework::standard::CommandResult, + model::interactions::{ButtonStyle, InteractionResponseType}, +}; + +use songbird::{ + create_player, ffmpeg, + input::{cached::Memory, Input}, + Event, +}; + +use crate::{ + event_handlers::RestartTrack, + framework::{Args, CommandInvoke, CreateGenericResponse}, + guild_data::CtxGuildData, + join_channel, play_cmd, + sound::Sound, + AudioIndex, MySQL, +}; + +use std::{convert::TryFrom, time::Duration}; + +#[command] +#[aliases("p")] +#[required_permissions(Managed)] +#[group("Play")] +#[description("Play a sound in your current voice channel")] +#[arg( + name = "query", + description = "Play sound with the specified name or ID", + kind = "String", + required = true +)] +#[example("`/play ubercharge` - play sound with name \"ubercharge\" ")] +#[example("`/play 13002` - play sound with ID 13002")] +pub async fn play( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + args: Args, +) -> CommandResult { + let guild = invoke.guild(ctx.cache.clone()).await.unwrap(); + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content(play_cmd(ctx, guild, invoke.author_id(), args, false).await), + ) + .await?; + + Ok(()) +} + +#[command("loop")] +#[required_permissions(Managed)] +#[group("Play")] +#[description("Play a sound on loop in your current voice channel")] +#[arg( + name = "query", + description = "Play sound with the specified name or ID", + kind = "String", + required = true +)] +#[example("`/loop rain` - loop sound with name \"rain\" ")] +#[example("`/loop 13002` - play sound with ID 13002")] +pub async fn loop_play( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + args: Args, +) -> CommandResult { + let guild = invoke.guild(ctx.cache.clone()).await.unwrap(); + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content(play_cmd(ctx, guild, invoke.author_id(), args, true).await), + ) + .await?; + + Ok(()) +} + +#[command("ambience")] +#[required_permissions(Managed)] +#[group("Play")] +#[description("Play ambient sound in your current voice channel")] +#[arg( + name = "name", + description = "Play sound with the specified name", + kind = "String", + required = false +)] +#[example("`/ambience rain on tent` - play the ambient sound \"rain on tent\" ")] +pub async fn play_ambience( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + args: Args, +) -> CommandResult { + let guild = invoke.guild(ctx.cache.clone()).await.unwrap(); + + let channel_to_join = guild + .voice_states + .get(&invoke.author_id()) + .and_then(|voice_state| voice_state.channel_id); + + match channel_to_join { + Some(user_channel) => { + let search_name = args.named("name").unwrap().to_lowercase(); + let audio_index = ctx.data.read().await.get::().cloned().unwrap(); + + if let Some(filename) = audio_index.get(&search_name) { + let (track, track_handler) = create_player( + Input::try_from( + Memory::new(ffmpeg(format!("audio/{}", filename)).await.unwrap()).unwrap(), + ) + .unwrap(), + ); + + let (call_handler, _) = join_channel(ctx, guild.clone(), user_channel).await; + let guild_data = ctx.guild_data(guild).await.unwrap(); + + { + let mut lock = call_handler.lock().await; + + lock.play(track); + } + + let _ = track_handler.set_volume(guild_data.read().await.volume as f32 / 100.0); + let _ = track_handler.add_event( + Event::Periodic( + track_handler.metadata().duration.unwrap() - Duration::from_millis(200), + None, + ), + RestartTrack {}, + ); + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content(format!("Playing ambience **{}**", search_name)), + ) + .await?; + } else { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { + e.title("Not Found").description(format!( + "Could not find ambience sound by name **{}** + +__Available ambience sounds:__ +{}", + search_name, + audio_index + .keys() + .into_iter() + .map(|i| i.as_str()) + .collect::>() + .join("\n") + )) + }), + ) + .await?; + } + } + + None => { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("You are not in a voice chat!"), + ) + .await?; + } + } + + Ok(()) +} + +#[command("soundboard")] +#[aliases("board")] +#[group("Play")] +#[description("Get a menu of sounds with buttons to play them")] +#[arg( + name = "1", + description = "Query for sound button 1", + kind = "String", + required = false +)] +#[arg( + name = "2", + description = "Query for sound button 2", + kind = "String", + required = false +)] +#[arg( + name = "3", + description = "Query for sound button 3", + kind = "String", + required = false +)] +#[arg( + name = "4", + description = "Query for sound button 4", + kind = "String", + required = false +)] +#[arg( + name = "5", + description = "Query for sound button 5", + kind = "String", + required = false +)] +#[arg( + name = "6", + description = "Query for sound button 6", + kind = "String", + required = false +)] +#[arg( + name = "7", + description = "Query for sound button 7", + kind = "String", + required = false +)] +#[arg( + name = "8", + description = "Query for sound button 8", + kind = "String", + required = false +)] +#[arg( + name = "9", + description = "Query for sound button 9", + kind = "String", + required = false +)] +#[arg( + name = "10", + description = "Query for sound button 10", + kind = "String", + required = false +)] +#[arg( + name = "11", + description = "Query for sound button 11", + kind = "String", + required = false +)] +#[arg( + name = "12", + description = "Query for sound button 12", + kind = "String", + required = false +)] +#[arg( + name = "13", + description = "Query for sound button 13", + kind = "String", + required = false +)] +#[arg( + name = "14", + description = "Query for sound button 14", + kind = "String", + required = false +)] +#[arg( + name = "15", + description = "Query for sound button 15", + kind = "String", + required = false +)] +#[arg( + name = "16", + description = "Query for sound button 16", + kind = "String", + required = false +)] +#[arg( + name = "17", + description = "Query for sound button 17", + kind = "String", + required = false +)] +#[arg( + name = "18", + description = "Query for sound button 18", + kind = "String", + required = false +)] +#[arg( + name = "19", + description = "Query for sound button 19", + kind = "String", + required = false +)] +#[arg( + name = "20", + description = "Query for sound button 20", + kind = "String", + required = false +)] +#[arg( + name = "21", + description = "Query for sound button 21", + kind = "String", + required = false +)] +#[arg( + name = "22", + description = "Query for sound button 22", + kind = "String", + required = false +)] +#[arg( + name = "23", + description = "Query for sound button 23", + kind = "String", + required = false +)] +#[arg( + name = "24", + description = "Query for sound button 24", + kind = "String", + required = false +)] +#[arg( + name = "25", + description = "Query for sound button 25", + kind = "String", + required = false +)] +#[example("`/soundboard ubercharge` - create a soundboard with a button for the \"ubercharge\" sound effect")] +#[example("`/soundboard 57000 24119 2 1002 13202` - create a soundboard with 5 buttons, for sounds with the IDs presented")] +pub async fn soundboard( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + args: Args, +) -> CommandResult { + if let Some(interaction) = invoke.interaction() { + let _ = interaction + .create_interaction_response(&ctx, |r| { + r.kind(InteractionResponseType::DeferredChannelMessageWithSource) + }) + .await; + } + + let pool = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + let mut sounds = vec![]; + + for n in 1..25 { + let search = Sound::search_for_sound( + args.named(&n.to_string()).unwrap_or(&"".to_string()), + invoke.guild_id().unwrap(), + invoke.author_id(), + pool.clone(), + true, + ) + .await?; + + if let Some(sound) = search.first() { + sounds.push(sound.clone()); + } + } + + invoke + .followup( + ctx.http.clone(), + CreateGenericResponse::new() + .content("**Play a sound:**") + .components(|c| { + for row in sounds.as_slice().chunks(5) { + let mut action_row: CreateActionRow = Default::default(); + for sound in row { + action_row.create_button(|b| { + b.style(ButtonStyle::Primary) + .label(&sound.name) + .custom_id(sound.id) + }); + } + + c.add_action_row(action_row); + } + + c + }), + ) + .await?; + + Ok(()) +} diff --git a/src/cmds/search.rs b/src/cmds/search.rs new file mode 100644 index 0000000..2335cb5 --- /dev/null +++ b/src/cmds/search.rs @@ -0,0 +1,214 @@ +use regex_command_attr::command; + +use serenity::{client::Context, framework::standard::CommandResult}; + +use crate::{ + framework::{Args, CommandInvoke, CreateGenericResponse}, + sound::Sound, + MySQL, +}; + +fn format_search_results(search_results: Vec) -> CreateGenericResponse { + let mut current_character_count = 0; + let title = "Public sounds matching filter:"; + + let field_iter = search_results + .iter() + .take(25) + .map(|item| { + ( + &item.name, + format!("ID: {}\nPlays: {}", item.id, item.plays), + true, + ) + }) + .filter(|item| { + current_character_count += item.0.len() + item.1.len(); + + current_character_count <= serenity::constants::MESSAGE_CODE_LIMIT - title.len() + }); + + CreateGenericResponse::new().embed(|e| e.title(title).fields(field_iter)) +} + +#[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 +)] +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"); + + let sounds; + let mut message_buffer; + + if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) { + sounds = Sound::get_user_sounds(invoke.author_id(), pool).await?; + + message_buffer = "All your sounds: ".to_string(); + } else { + sounds = Sound::get_guild_sounds(invoke.guild_id().unwrap(), pool).await?; + + message_buffer = "All sounds on this server: ".to_string(); + } + + for sound in sounds { + message_buffer.push_str( + format!( + "**{}** ({}), ", + sound.name, + if sound.public { "🔓" } else { "🔒" } + ) + .as_str(), + ); + + if message_buffer.len() > 2000 { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content(message_buffer), + ) + .await?; + + message_buffer = "".to_string(); + } + } + + if message_buffer.len() > 0 { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content(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"); + + let query = args.named("query").unwrap(); + + let search_results = Sound::search_for_sound( + query, + invoke.guild_id().unwrap(), + invoke.author_id(), + pool, + false, + ) + .await?; + + invoke + .respond(ctx.http.clone(), format_search_results(search_results)) + .await?; + + Ok(()) +} + +#[command("popular")] +#[group("Search")] +#[description("Show popular sounds")] +pub async fn show_popular_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"); + + let search_results = sqlx::query_as_unchecked!( + Sound, + " +SELECT name, id, plays, public, server_id, uploader_id + FROM sounds + WHERE public = 1 + ORDER BY plays DESC + LIMIT 25 + " + ) + .fetch_all(&pool) + .await?; + + invoke + .respond(ctx.http.clone(), format_search_results(search_results)) + .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"); + + let search_results = sqlx::query_as_unchecked!( + Sound, + " +SELECT name, id, plays, public, server_id, uploader_id + FROM sounds + WHERE public = 1 + ORDER BY rand() + LIMIT 25 + " + ) + .fetch_all(&pool) + .await + .unwrap(); + + invoke + .respond(ctx.http.clone(), format_search_results(search_results)) + .await?; + + Ok(()) +} diff --git a/src/cmds/settings.rs b/src/cmds/settings.rs new file mode 100644 index 0000000..6d41f7f --- /dev/null +++ b/src/cmds/settings.rs @@ -0,0 +1,351 @@ +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, +}; + +#[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 +)] +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; + 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; + + guild_data.read().await.commit(pool).await?; + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content(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)] +#[allow_slash(false)] +#[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 { + 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)] +#[allow_slash(false)] +#[group("Settings")] +#[description("Change the roles allowed to use the bot")] +pub async fn set_allowed_roles( + ctx: &Context, + invoke: &(dyn CommandInvoke + Sync + Send), + args: Args, +) -> CommandResult { + let msg = invoke.msg().unwrap(); + let guild_id = *msg.guild_id.unwrap().as_u64(); + + let pool = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + if args.is_empty() { + let roles = sqlx::query!( + " +SELECT role + FROM roles + WHERE guild_id = ? + ", + guild_id + ) + .fetch_all(&pool) + .await?; + + let all_roles = roles + .iter() + .map(|i| format!("<@&{}>", i.role.to_string())) + .collect::>() + .join(", "); + + msg.channel_id.say(&ctx, format!("Usage: `?roles `. Current roles: {}", all_roles)).await?; + } else { + sqlx::query!( + " +DELETE FROM roles + WHERE guild_id = ? + ", + guild_id + ) + .execute(&pool) + .await?; + + if msg.mention_roles.len() > 0 { + for role in msg.mention_roles.iter().map(|r| *r.as_u64()) { + sqlx::query!( + " +INSERT INTO roles (guild_id, role) + VALUES + (?, ?) + ", + guild_id, + role + ) + .execute(&pool) + .await?; + } + + msg.channel_id + .say(&ctx, "Specified roles whitelisted") + .await?; + } else { + sqlx::query!( + " +INSERT INTO roles (guild_id, role) + VALUES + (?, ?) + ", + guild_id, + guild_id + ) + .execute(&pool) + .await?; + + msg.channel_id + .say(&ctx, "Role whitelisting disabled") + .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 +)] +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, + ) + .await?; + + match sound_vec.first() { + Some(sound) => { + ctx.update_join_sound(user_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?; + } + + None => { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content("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)] +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"); + + let guild_data_opt = ctx.guild_data(invoke.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 = !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" } + )), + ) + .await?; + } + + Ok(()) +} diff --git a/src/cmds/stop.rs b/src/cmds/stop.rs new file mode 100644 index 0000000..eac7e5c --- /dev/null +++ b/src/cmds/stop.rs @@ -0,0 +1,56 @@ +use regex_command_attr::command; + +use serenity::{client::Context, framework::standard::CommandResult}; + +use crate::framework::{Args, CommandInvoke, CreateGenericResponse}; + +use songbird; + +#[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); + + if let Some(call) = call_opt { + let mut lock = call.lock().await; + + lock.stop(); + } + + invoke + .respond(ctx.http.clone(), CreateGenericResponse::new().content("👍")) + .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(); + + let songbird = songbird::get(ctx).await.unwrap(); + let _ = songbird.leave(guild_id).await; + + invoke + .respond(ctx.http.clone(), CreateGenericResponse::new().content("👍")) + .await?; + + Ok(()) +} diff --git a/src/framework.rs b/src/framework.rs index a4035ee..9bb93f2 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -70,10 +70,6 @@ impl Args { Self { args } } - pub fn len(&self) -> usize { - self.args.len() - } - pub fn is_empty(&self) -> bool { self.args.is_empty() } @@ -345,7 +341,7 @@ pub struct Command { pub desc: &'static str, pub examples: &'static [&'static str], - pub group: Option<&'static str>, + pub group: &'static str, pub allow_slash: bool, pub required_permissions: PermissionLevel, @@ -440,7 +436,7 @@ impl fmt::Debug for Command { pub struct RegexFramework { pub commands: HashMap, - commands_: HashSet<&'static Command>, + pub commands_: HashSet<&'static Command>, command_matcher: Regex, default_prefix: String, client_id: u64, diff --git a/src/main.rs b/src/main.rs index 74e6c27..6b66413 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate lazy_static; +mod cmds; mod error; mod event_handlers; mod framework; @@ -8,37 +9,25 @@ mod guild_data; mod sound; use crate::{ - event_handlers::{Handler, RestartTrack}, - framework::{Args, CommandInvoke, CreateGenericResponse, RegexFramework}, + event_handlers::Handler, + framework::{Args, RegexFramework}, guild_data::{CtxGuildData, GuildData}, - sound::{JoinSoundCtx, Sound}, + sound::Sound, }; use log::info; -use regex_command_attr::command; - use serenity::{ - builder::CreateActionRow, client::{bridge::gateway::GatewayIntents, Client, Context}, - framework::standard::CommandResult, http::Http, model::{ guild::Guild, - id::{ChannelId, GuildId, RoleId, UserId}, - interactions::ButtonStyle, + id::{ChannelId, GuildId, UserId}, }, - prelude::*, + prelude::{Mutex, TypeMapKey}, }; -use songbird::{ - create_player, - error::JoinResult, - ffmpeg, - input::{cached::Memory, Input}, - tracks::TrackHandle, - Call, Event, SerenityInit, -}; +use songbird::{create_player, error::JoinResult, tracks::TrackHandle, Call, SerenityInit}; use sqlx::mysql::MySqlPool; @@ -46,9 +35,8 @@ use dotenv::dotenv; use dashmap::DashMap; -use std::{collections::HashMap, convert::TryFrom, env, sync::Arc, time::Duration}; +use std::{collections::HashMap, env, sync::Arc}; -use serenity::model::prelude::InteractionResponseType; use tokio::sync::{MutexGuard, RwLock}; struct MySQL; @@ -152,6 +140,65 @@ async fn join_channel( (call, res) } +async fn play_cmd(ctx: &Context, guild: Guild, user_id: UserId, args: Args, 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 search_term = args.named("query").unwrap(); + + let pool = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Could not get SQLPool from data"); + + let mut sound_vec = + Sound::search_for_sound(search_term, 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, 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(), + } +} + // entry point #[tokio::main] async fn main() -> Result<(), Box> { @@ -187,34 +234,32 @@ async fn main() -> Result<(), Box> { .case_insensitive(true) .ignore_bots(true) // info commands - .add_command(&HELP_COMMAND) - .add_command(&INFO_COMMAND) - .add_command(&INFO_COMMAND) - .add_command(&INFO_COMMAND) + .add_command(&cmds::info::HELP_COMMAND) + .add_command(&cmds::info::INFO_COMMAND) // play commands - .add_command(&LOOP_PLAY_COMMAND) - .add_command(&PLAY_COMMAND) - .add_command(&STOP_PLAYING_COMMAND) - .add_command(&DISCONNECT_COMMAND) - .add_command(&SOUNDBOARD_COMMAND) + .add_command(&cmds::play::LOOP_PLAY_COMMAND) + .add_command(&cmds::play::PLAY_COMMAND) + .add_command(&cmds::play::SOUNDBOARD_COMMAND) + .add_command(&cmds::stop::STOP_PLAYING_COMMAND) + .add_command(&cmds::stop::DISCONNECT_COMMAND) // sound management commands - .add_command(&UPLOAD_NEW_SOUND_COMMAND) - .add_command(&DELETE_SOUND_COMMAND) - .add_command(&LIST_SOUNDS_COMMAND) - .add_command(&CHANGE_PUBLIC_COMMAND) + .add_command(&cmds::manage::UPLOAD_NEW_SOUND_COMMAND) + .add_command(&cmds::manage::DELETE_SOUND_COMMAND) + .add_command(&cmds::manage::CHANGE_PUBLIC_COMMAND) // setting commands - .add_command(&CHANGE_PREFIX_COMMAND) - .add_command(&SET_ALLOWED_ROLES_COMMAND) - .add_command(&CHANGE_VOLUME_COMMAND) - .add_command(&ALLOW_GREET_SOUNDS_COMMAND) - .add_command(&SET_GREET_SOUND_COMMAND) + .add_command(&cmds::settings::CHANGE_PREFIX_COMMAND) + .add_command(&cmds::settings::SET_ALLOWED_ROLES_COMMAND) + .add_command(&cmds::settings::CHANGE_VOLUME_COMMAND) + .add_command(&cmds::settings::ALLOW_GREET_SOUNDS_COMMAND) + .add_command(&cmds::settings::SET_GREET_SOUND_COMMAND) // search commands - .add_command(&SEARCH_SOUNDS_COMMAND) - .add_command(&SHOW_POPULAR_SOUNDS_COMMAND) - .add_command(&SHOW_RANDOM_SOUNDS_COMMAND); + .add_command(&cmds::search::LIST_SOUNDS_COMMAND) + .add_command(&cmds::search::SEARCH_SOUNDS_COMMAND) + .add_command(&cmds::search::SHOW_POPULAR_SOUNDS_COMMAND) + .add_command(&cmds::search::SHOW_RANDOM_SOUNDS_COMMAND); if audio_index.is_some() { - framework = framework.add_command(&PLAY_AMBIENCE_COMMAND); + framework = framework.add_command(&cmds::play::PLAY_AMBIENCE_COMMAND); } framework = framework.build(); @@ -298,1471 +343,3 @@ async fn main() -> Result<(), Box> { Ok(()) } - -#[command] -#[description("Get information on the commands of the bot")] -#[arg( - name = "command", - description = "Get help for a specific command", - kind = "String", - required = false -)] -async fn help( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - if let Some(command_name) = args.named("command") { - let framework = ctx - .data - .read() - .await - .get::() - .cloned() - .unwrap(); - - if let Some(command) = framework.commands.get(command_name) { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().embed(|e| { - e.title(format!("{} Help", command_name)) - .color(THEME_COLOR) - .description(format!( - "**Aliases** -{} - -**Overview** - • {} - -**Arguments** -{} - -**Examples** -{}", - command - .names - .iter() - .map(|n| format!("`{}`", n)) - .collect::>() - .join(" "), - command.desc, - command - .args - .iter() - .map(|a| format!( - " • `{}` {} - {}", - a.name, - if a.required { "" } else { "[optional]" }, - a.description - )) - .collect::>() - .join("\n"), - command - .examples - .iter() - .map(|e| format!(" • {}", e)) - .collect::>() - .join("\n") - )) - }), - ) - .await?; - } else { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().embed(|e| { - e.title("Invalid Command") - .color(THEME_COLOR) - .description("") - }), - ) - .await?; - } - } else { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .embed(|e| e.title("Help").color(THEME_COLOR).description("")), - ) - .await?; - } - - Ok(()) -} - -#[command] -#[aliases("p")] -#[required_permissions(Managed)] -#[description("Play a sound in your current voice channel")] -#[arg( - name = "query", - description = "Play sound with the specified name or ID", - kind = "String", - required = true -)] -async fn play( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - let guild = invoke.guild(ctx.cache.clone()).await.unwrap(); - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(play_cmd(ctx, guild, invoke.author_id(), args, false).await), - ) - .await?; - - Ok(()) -} - -#[command("loop")] -#[required_permissions(Managed)] -#[description("Play a sound on loop in your current voice channel")] -#[arg( - name = "query", - description = "Play sound with the specified name or ID", - kind = "String", - required = true -)] -async fn loop_play( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - let guild = invoke.guild(ctx.cache.clone()).await.unwrap(); - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(play_cmd(ctx, guild, invoke.author_id(), args, true).await), - ) - .await?; - - Ok(()) -} - -async fn play_cmd(ctx: &Context, guild: Guild, user_id: UserId, args: Args, 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 search_term = args.named("query").unwrap(); - - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); - - let mut sound_vec = - Sound::search_for_sound(search_term, 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, 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(), - } -} - -#[command("ambience")] -#[required_permissions(Managed)] -#[description("Play ambient sound in your current voice channel")] -#[arg( - name = "name", - description = "Play sound with the specified name", - kind = "String", - required = false -)] -async fn play_ambience( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - let guild = invoke.guild(ctx.cache.clone()).await.unwrap(); - - let channel_to_join = guild - .voice_states - .get(&invoke.author_id()) - .and_then(|voice_state| voice_state.channel_id); - - match channel_to_join { - Some(user_channel) => { - let search_name = args.named("name").unwrap().to_lowercase(); - let audio_index = ctx.data.read().await.get::().cloned().unwrap(); - - if let Some(filename) = audio_index.get(&search_name) { - let (track, track_handler) = create_player( - Input::try_from( - Memory::new(ffmpeg(format!("audio/{}", filename)).await.unwrap()).unwrap(), - ) - .unwrap(), - ); - - let (call_handler, _) = join_channel(ctx, guild.clone(), user_channel).await; - let guild_data = ctx.guild_data(guild).await.unwrap(); - - { - let mut lock = call_handler.lock().await; - - lock.play(track); - } - - let _ = track_handler.set_volume(guild_data.read().await.volume as f32 / 100.0); - let _ = track_handler.add_event( - Event::Periodic( - track_handler.metadata().duration.unwrap() - Duration::from_millis(200), - None, - ), - RestartTrack {}, - ); - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(format!("Playing ambience **{}**", search_name)), - ) - .await?; - } else { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().embed(|e| { - e.title("Not Found").description(format!( - "Could not find ambience sound by name **{}** - -__Available ambience sounds:__ -{}", - search_name, - audio_index - .keys() - .into_iter() - .map(|i| i.as_str()) - .collect::>() - .join("\n") - )) - }), - ) - .await?; - } - } - - None => { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("You are not in a voice chat!"), - ) - .await?; - } - } - - Ok(()) -} - -#[command("stop")] -#[required_permissions(Managed)] -#[description("Stop the bot from playing")] -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); - - if let Some(call) = call_opt { - let mut lock = call.lock().await; - - lock.stop(); - } - - invoke - .respond(ctx.http.clone(), CreateGenericResponse::new().content("👍")) - .await?; - - Ok(()) -} - -#[command] -#[aliases("dc")] -#[required_permissions(Managed)] -#[description("Disconnect the bot")] -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(); - let _ = songbird.leave(guild_id).await; - - invoke - .respond(ctx.http.clone(), CreateGenericResponse::new().content("👍")) - .await?; - - Ok(()) -} - -#[command] -#[aliases("invite")] -#[description("Get additional information on the bot")] -async fn info( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - _args: Args, -) -> CommandResult { - let current_user = ctx.cache.current_user().await; - - invoke.respond(ctx.http.clone(), CreateGenericResponse::new() - .embed(|e| e - .title("Info") - .color(THEME_COLOR) - .footer(|f| f - .text(concat!(env!("CARGO_PKG_NAME"), " ver ", env!("CARGO_PKG_VERSION")))) - .description(format!("Default prefix: `?` - -Reset prefix: `@{0} prefix ?` - -Invite me: https://discord.com/api/oauth2/authorize?client_id={1}&permissions=3165184&scope=applications.commands%20bot - -**Welcome to SoundFX!** -Developer: <@203532103185465344> -Find me on https://discord.jellywx.com/ and on https://github.com/JellyWX :) - -**Sound Credits** -\"The rain falls against the parasol\" https://freesound.org/people/straget/ -\"Heavy Rain\" https://freesound.org/people/lebaston100/ -\"Rain on Windows, Interior, A\" https://freesound.org/people/InspectorJ/ -\"Seaside Waves, Close, A\" https://freesound.org/people/InspectorJ/ -\"Small River 1 - Fast - Close\" https://freesound.org/people/Pfannkuchn/ - -**An online dashboard is available!** Visit https://soundfx.jellywx.com/dashboard -There is a maximum sound limit per user. This can be removed by subscribing at **https://patreon.com/jellywx**", current_user.name, current_user.id.as_u64())))).await?; - - Ok(()) -} - -#[command("volume")] -#[aliases("vol")] -#[required_permissions(Managed)] -#[description("Change the bot's volume in this server")] -#[arg( - name = "volume", - description = "New volume for the bot to use", - kind = "Integer", - required = false -)] -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; - 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; - - guild_data.read().await.commit(pool).await?; - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(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)] -#[allow_slash(false)] -#[arg( - name = "prefix", - kind = "String", - description = "The new prefix to use for the bot", - required = true -)] -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 { - 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("upload")] -#[description("Upload a new sound to the bot")] -#[arg( - name = "name", - description = "Name to upload sound to", - kind = "String", - required = true -)] -async fn upload_new_sound( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - fn is_numeric(s: &String) -> bool { - for char in s.chars() { - if char.is_digit(10) { - continue; - } else { - return false; - } - } - true - } - - let new_name = args - .named("name") - .map(|n| n.to_string()) - .unwrap_or(String::new()); - - if !new_name.is_empty() && new_name.len() <= 20 { - if !is_numeric(&new_name) { - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); - - // need to check the name is not currently in use by the user - let count_name = - Sound::count_named_user_sounds(invoke.author_id().0, &new_name, pool.clone()) - .await?; - if count_name > 0 { - invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("You are already using that name. Please choose a unique name for your upload.")).await?; - } else { - // need to check how many sounds user currently has - let count = Sound::count_user_sounds(invoke.author_id().0, pool.clone()).await?; - let mut permit_upload = true; - - // need to check if user is patreon or nah - if count >= *MAX_SOUNDS { - let patreon_guild_member = GuildId(*PATREON_GUILD) - .member(ctx, invoke.author_id()) - .await; - - if let Ok(member) = patreon_guild_member { - permit_upload = member.roles.contains(&RoleId(*PATREON_ROLE)); - } else { - permit_upload = false; - } - } - - if permit_upload { - let attachment = if let Some(attachment) = invoke - .msg() - .map(|m| m.attachments.get(0).map(|a| a.url.clone())) - .flatten() - { - Some(attachment) - } else { - invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Please now upload an audio file under 1MB in size (larger files will be automatically trimmed):")).await?; - - let reply = invoke - .channel_id() - .await_reply(&ctx) - .author_id(invoke.author_id()) - .timeout(Duration::from_secs(120)) - .await; - - match reply { - Some(reply_msg) => { - if let Some(attachment) = reply_msg.attachments.get(0) { - Some(attachment.url.clone()) - } else { - invoke.followup(ctx.http.clone(), CreateGenericResponse::new().content("Please upload 1 attachment following your upload command. Aborted")).await?; - - None - } - } - - None => { - invoke - .followup( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Upload timed out. Please redo the command"), - ) - .await?; - - None - } - } - }; - - if let Some(url) = attachment { - match Sound::create_anon( - &new_name, - url.as_str(), - invoke.guild_id().unwrap().0, - invoke.author_id().0, - pool, - ) - .await - { - Ok(_) => { - invoke - .followup( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Sound has been uploaded"), - ) - .await?; - } - - Err(e) => { - println!("Error occurred during upload: {:?}", e); - invoke - .followup( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Sound failed to upload."), - ) - .await?; - } - } - } - } else { - invoke.respond( - ctx.http.clone(), - CreateGenericResponse::new().content(format!( - "You have reached the maximum number of sounds ({}). Either delete some with `?delete` or join our Patreon for unlimited uploads at **https://patreon.com/jellywx**", - *MAX_SOUNDS, - ))).await?; - } - } - } else { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Please ensure the sound name contains a non-numerical character"), - ) - .await?; - } - } else { - invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("Usage: `?upload `. Please ensure the name provided is less than 20 characters in length")).await?; - } - - Ok(()) -} - -#[command("roles")] -#[required_permissions(Restricted)] -#[allow_slash(false)] -async fn set_allowed_roles( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - let msg = invoke.msg().unwrap(); - let guild_id = *msg.guild_id.unwrap().as_u64(); - - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); - - if args.is_empty() { - let roles = sqlx::query!( - " -SELECT role - FROM roles - WHERE guild_id = ? - ", - guild_id - ) - .fetch_all(&pool) - .await?; - - let all_roles = roles - .iter() - .map(|i| format!("<@&{}>", i.role.to_string())) - .collect::>() - .join(", "); - - msg.channel_id.say(&ctx, format!("Usage: `?roles `. Current roles: {}", all_roles)).await?; - } else { - sqlx::query!( - " -DELETE FROM roles - WHERE guild_id = ? - ", - guild_id - ) - .execute(&pool) - .await?; - - if msg.mention_roles.len() > 0 { - for role in msg.mention_roles.iter().map(|r| *r.as_u64()) { - sqlx::query!( - " -INSERT INTO roles (guild_id, role) - VALUES - (?, ?) - ", - guild_id, - role - ) - .execute(&pool) - .await?; - } - - msg.channel_id - .say(&ctx, "Specified roles whitelisted") - .await?; - } else { - sqlx::query!( - " -INSERT INTO roles (guild_id, role) - VALUES - (?, ?) - ", - guild_id, - guild_id - ) - .execute(&pool) - .await?; - - msg.channel_id - .say(&ctx, "Role whitelisting disabled") - .await?; - } - } - - Ok(()) -} - -#[command("list")] -#[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 -)] -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"); - - let sounds; - let mut message_buffer; - - if args.named("me").map(|i| i.to_owned()) == Some("me".to_string()) { - sounds = Sound::get_user_sounds(invoke.author_id(), pool).await?; - - message_buffer = "All your sounds: ".to_string(); - } else { - sounds = Sound::get_guild_sounds(invoke.guild_id().unwrap(), pool).await?; - - message_buffer = "All sounds on this server: ".to_string(); - } - - for sound in sounds { - message_buffer.push_str( - format!( - "**{}** ({}), ", - sound.name, - if sound.public { "🔓" } else { "🔒" } - ) - .as_str(), - ); - - if message_buffer.len() > 2000 { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(message_buffer), - ) - .await?; - - message_buffer = "".to_string(); - } - } - - if message_buffer.len() > 0 { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(message_buffer), - ) - .await?; - } - - Ok(()) -} - -#[command("soundboard")] -#[aliases("board")] -#[description("Get a menu of sounds with buttons to play them")] -#[arg( - name = "1", - description = "Query for sound button 1", - kind = "String", - required = false -)] -#[arg( - name = "2", - description = "Query for sound button 2", - kind = "String", - required = false -)] -#[arg( - name = "3", - description = "Query for sound button 3", - kind = "String", - required = false -)] -#[arg( - name = "4", - description = "Query for sound button 4", - kind = "String", - required = false -)] -#[arg( - name = "5", - description = "Query for sound button 5", - kind = "String", - required = false -)] -#[arg( - name = "6", - description = "Query for sound button 6", - kind = "String", - required = false -)] -#[arg( - name = "7", - description = "Query for sound button 7", - kind = "String", - required = false -)] -#[arg( - name = "8", - description = "Query for sound button 8", - kind = "String", - required = false -)] -#[arg( - name = "9", - description = "Query for sound button 9", - kind = "String", - required = false -)] -#[arg( - name = "10", - description = "Query for sound button 10", - kind = "String", - required = false -)] -#[arg( - name = "11", - description = "Query for sound button 11", - kind = "String", - required = false -)] -#[arg( - name = "12", - description = "Query for sound button 12", - kind = "String", - required = false -)] -#[arg( - name = "13", - description = "Query for sound button 13", - kind = "String", - required = false -)] -#[arg( - name = "14", - description = "Query for sound button 14", - kind = "String", - required = false -)] -#[arg( - name = "15", - description = "Query for sound button 15", - kind = "String", - required = false -)] -#[arg( - name = "16", - description = "Query for sound button 16", - kind = "String", - required = false -)] -#[arg( - name = "17", - description = "Query for sound button 17", - kind = "String", - required = false -)] -#[arg( - name = "18", - description = "Query for sound button 18", - kind = "String", - required = false -)] -#[arg( - name = "19", - description = "Query for sound button 19", - kind = "String", - required = false -)] -#[arg( - name = "20", - description = "Query for sound button 20", - kind = "String", - required = false -)] -#[arg( - name = "21", - description = "Query for sound button 21", - kind = "String", - required = false -)] -#[arg( - name = "22", - description = "Query for sound button 22", - kind = "String", - required = false -)] -#[arg( - name = "23", - description = "Query for sound button 23", - kind = "String", - required = false -)] -#[arg( - name = "24", - description = "Query for sound button 24", - kind = "String", - required = false -)] -#[arg( - name = "25", - description = "Query for sound button 25", - kind = "String", - required = false -)] -async fn soundboard( - ctx: &Context, - invoke: &(dyn CommandInvoke + Sync + Send), - args: Args, -) -> CommandResult { - if let Some(interaction) = invoke.interaction() { - let _ = interaction - .create_interaction_response(&ctx, |r| { - r.kind(InteractionResponseType::DeferredChannelMessageWithSource) - }) - .await; - } - - let pool = ctx - .data - .read() - .await - .get::() - .cloned() - .expect("Could not get SQLPool from data"); - - let mut sounds = vec![]; - - for n in 1..25 { - let search = Sound::search_for_sound( - args.named(&n.to_string()).unwrap_or(&"".to_string()), - invoke.guild_id().unwrap(), - invoke.author_id(), - pool.clone(), - true, - ) - .await?; - - if let Some(sound) = search.first() { - sounds.push(sound.clone()); - } - } - - invoke - .followup( - ctx.http.clone(), - CreateGenericResponse::new() - .content("**Play a sound:**") - .components(|c| { - for row in sounds.as_slice().chunks(5) { - let mut action_row: CreateActionRow = Default::default(); - for sound in row { - action_row.create_button(|b| { - b.style(ButtonStyle::Primary) - .label(&sound.name) - .custom_id(sound.id) - }); - } - - c.add_action_row(action_row); - } - - c - }), - ) - .await?; - - Ok(()) -} - -#[command("public")] -#[description("Change a sound between public and private")] -#[arg( - name = "query", - kind = "String", - description = "Sound name or ID to change the privacy setting of", - required = true -)] -async fn change_public( - 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 uid = invoke.author_id().as_u64().to_owned(); - - let name = args.named("query").unwrap(); - let gid = *invoke.guild_id().unwrap().as_u64(); - - let mut sound_vec = Sound::search_for_sound(name, gid, uid, pool.clone(), true).await?; - let sound_result = sound_vec.first_mut(); - - match sound_result { - Some(sound) => { - if sound.uploader_id != Some(uid) { - invoke.respond(ctx.http.clone(), CreateGenericResponse::new().content("You can only change the visibility of sounds you have uploaded. Use `?list me` to view your sounds")).await?; - } else { - if sound.public { - sound.public = false; - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Sound has been set to private 🔒"), - ) - .await?; - } else { - sound.public = true; - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Sound has been set to public 🔓"), - ) - .await?; - } - - sound.commit(pool).await? - } - } - - None => { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Sound could not be found by that name."), - ) - .await?; - } - } - - Ok(()) -} - -#[command("delete")] -#[description("Delete a sound you have uploaded")] -#[arg( - name = "query", - description = "Delete sound with the specified name or ID", - kind = "String", - required = true -)] -async fn delete_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 uid = invoke.author_id().0; - let gid = invoke.guild_id().unwrap().0; - - let name = args - .named("query") - .map(|s| s.to_owned()) - .unwrap_or(String::new()); - - let sound_vec = Sound::search_for_sound(&name, gid, uid, pool.clone(), true).await?; - let sound_result = sound_vec.first(); - - match sound_result { - Some(sound) => { - if sound.uploader_id != Some(uid) && sound.server_id != gid { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content( - "You can only delete sounds from this guild or that you have uploaded.", - ), - ) - .await?; - } else { - let has_perms = { - if let Ok(member) = invoke.member(&ctx).await { - if let Ok(perms) = member.permissions(&ctx).await { - perms.manage_guild() - } else { - false - } - } else { - false - } - }; - - if sound.uploader_id == Some(uid) || has_perms { - sound.delete(pool).await?; - - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Sound has been deleted"), - ) - .await?; - } else { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content( - "Only server admins can delete sounds uploaded by other users.", - ), - ) - .await?; - } - } - } - - None => { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Sound could not be found by that name."), - ) - .await?; - } - } - - Ok(()) -} - -fn format_search_results(search_results: Vec) -> CreateGenericResponse { - let mut current_character_count = 0; - let title = "Public sounds matching filter:"; - - let field_iter = search_results - .iter() - .take(25) - .map(|item| { - ( - &item.name, - format!("ID: {}\nPlays: {}", item.id, item.plays), - true, - ) - }) - .filter(|item| { - current_character_count += item.0.len() + item.1.len(); - - current_character_count <= 2048 - title.len() - }); - - CreateGenericResponse::new().embed(|e| e.title(title).fields(field_iter)) -} - -#[command("search")] -#[description("Search for sounds")] -#[arg( - name = "query", - kind = "String", - description = "Sound name to search for", - required = true -)] -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"); - - let query = args.named("query").unwrap(); - - let search_results = Sound::search_for_sound( - query, - invoke.guild_id().unwrap(), - invoke.author_id(), - pool, - false, - ) - .await?; - - invoke - .respond(ctx.http.clone(), format_search_results(search_results)) - .await?; - - Ok(()) -} - -#[command("popular")] -#[description("Show popular sounds")] -async fn show_popular_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"); - - let search_results = sqlx::query_as_unchecked!( - Sound, - " -SELECT name, id, plays, public, server_id, uploader_id - FROM sounds - WHERE public = 1 - ORDER BY plays DESC - LIMIT 25 - " - ) - .fetch_all(&pool) - .await?; - - invoke - .respond(ctx.http.clone(), format_search_results(search_results)) - .await?; - - Ok(()) -} - -#[command("random")] -#[description("Show a page of random sounds")] -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"); - - let search_results = sqlx::query_as_unchecked!( - Sound, - " -SELECT name, id, plays, public, server_id, uploader_id - FROM sounds - WHERE public = 1 - ORDER BY rand() - LIMIT 25 - " - ) - .fetch_all(&pool) - .await - .unwrap(); - - invoke - .respond(ctx.http.clone(), format_search_results(search_results)) - .await?; - - Ok(()) -} - -#[command("greet")] -#[description("Set a join sound")] -#[arg( - name = "query", - kind = "String", - description = "Name or ID of sound to set as your greet sound", - required = false -)] -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, - ) - .await?; - - match sound_vec.first() { - Some(sound) => { - ctx.update_join_sound(user_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?; - } - - None => { - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Could not find a sound by that name."), - ) - .await?; - } - } - } - - Ok(()) -} - -#[command("allow_greet")] -#[description("Configure whether users should be able to use join sounds")] -#[required_permissions(Restricted)] -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"); - - let guild_data_opt = ctx.guild_data(invoke.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 = !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" } - )), - ) - .await?; - } - - Ok(()) -}