use std::collections::HashMap; use std::sync::Arc; use log::debug; use serenity::model::application::interaction::{InteractionResponseType, application_command::ApplicationCommandInteraction}; use serenity::model::prelude::{GuildId, ChannelId}; use serenity::model::user::User; use serenity::prelude::*; use songbird::{Call, Songbird}; use songbird::input::{Restartable, Input, Metadata, error::Error as SongbirdError}; pub mod pause; pub mod play; pub mod resume; pub mod skip; pub mod stop; pub mod volume; #[derive(Clone, Debug)] pub struct AudioConfigs; impl TypeMapKey for AudioConfigs { type Value = Arc>>; } #[derive(Clone, Debug)] pub struct AudioConfig { pub volume: f32 } /// Joins a Discord voice channel. /// /// # Arguments /// - ctx - The context of the command. /// - guild_id_option - The guild ID of the guild to join. /// - user - The user that is requesting to join the voice channel. /// /// # Returns /// Result<(), String> - Ok if the bot successfully joined the voice channel, Err if there was an error. pub async fn join(ctx: &Context, guild_id_option: &Option, user: &User) -> Result<(), String> { let guild_id = match guild_id_option { Some(g) => g, None => { return Err(format!("{}", "No guild ID set")); } }; let channel_id = match find_voice_channel(&ctx, &guild_id, &user) { Ok(channel) => channel, Err(err) => return Err(format!("{}", err)) }; debug!("<{}> Joining channel {}", guild_id.0, channel_id); let manager = get_songbird(ctx).await; let (_handle_lock, success) = manager.join(guild_id.to_owned(), channel_id.to_owned()).await; match success { Ok(s) => Ok(s), Err(err) => Err(format!("{}", err)) } } /// Leaves a Discord voice channel. /// /// # Arguments /// - ctx - The context of the command. /// - guild_id_option - The guild ID of the guild to leave. /// /// # Returns /// Result<(), String> - Ok if the bot successfully left the voice channel, Err if there was an error. pub async fn leave(ctx: &Context, guild_id_option: &Option) -> Result<(), String> { let guild_id = match guild_id_option { Some(g) => g, None => { return Err(format!("{}", "No guild ID set")); } }; let manager = get_songbird(ctx).await; if manager.get(*guild_id).is_some() { debug!("<{}> Disconnecting from channel", guild_id.0); if let Err(e) = manager.remove(*guild_id).await { return Err(format!("{}", e)) } } Ok(()) } /// Finds the voice channel that the user is in. /// /// # Arguments /// - ctx - The context of the command. /// - guild_id - The guild ID of the guild to search. /// - user - The user to search for. /// /// # Returns /// Result - Ok if the user is in a voice channel, Err if the user is not in a voice channel. fn find_voice_channel(ctx: &Context, guild_id: &GuildId, user: &User) -> Result { let guild = match guild_id.to_guild_cached(ctx.cache.to_owned()) { Some(g) => g, None => return Err(format!("Guild not found")) }; match guild.voice_states.get(&user.id).and_then(|voice_state| voice_state.channel_id) { Some(channel) => Ok(channel), None => return Err(format!("User is not in a voice channel")) } } /// Creates a response to an interaction. /// /// # Arguments /// - ctx - The context of the command. /// - command - The command that was sent. /// - content - The content of the response. /// /// # Returns /// Result<(), SerenityError> - Ok if the response was created successfully, Err if there was an error. pub async fn create_response(ctx: &Context, command: &ApplicationCommandInteraction, content: String) -> Result<(), SerenityError> { command.create_interaction_response(&ctx.http, |response: &mut serenity::builder::CreateInteractionResponse<'_>| { response .kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|message: &mut serenity::builder::CreateInteractionResponseData<'_>| message.content(content)) }).await } /// Edits a response to an interaction. /// /// # Arguments /// - ctx - The context of the command. /// - command - The command that was sent. /// - content - The content of the response. /// /// # Returns /// Result - Ok if the response was edited successfully, Err if there was an error. pub async fn edit_response(ctx: &Context, command: &ApplicationCommandInteraction, content: String) -> Result { command.edit_original_interaction_response(&ctx.http, |response: &mut serenity::builder::EditInteractionResponse| { response.content(content) }).await } /// Adds a song to the queue. /// /// # Arguments /// - call - The call to add the song to. /// - url - The URL of the song to add. /// - lazy - Whether or not to lazy load the song. /// /// # Returns /// Result - Ok if the song was added successfully, Err if there was an error. pub async fn add_song(call: Arc>, url: &str, lazy: bool, audio_config: Option<&AudioConfig>) -> Result { let source = if is_valid_url(url) { Restartable::ytdl(url.to_owned(), lazy).await? } else { Restartable::ytdl_search(url, lazy).await? }; let mut handler = call.lock().await; let track: Input = source.into(); let metadata = *track.metadata.clone(); let track_handle = handler.enqueue_source(track); if let Some(ac) = audio_config { let _ = track_handle.set_volume(ac.volume); } Ok(metadata) } /// Checks if a string is a valid URL. /// /// # Arguments /// - url - The string to check. /// /// # Returns /// bool - True if the string is a valid URL, false if it is not. fn is_valid_url(url: &str) -> bool { match url.parse::() { Ok(_) => return true, Err(_) => return false } } /// Gets the Songbird voice client. /// /// # Arguments /// - ctx - The context of the command. /// /// # Returns /// Arc - The Songbird voice client. pub async fn get_songbird(ctx: &Context) -> Arc { songbird::get(ctx).await.expect("Songbird Voice client placed in at initialization") }