diff --git a/bot-serenityutils/src/error.rs b/bot-serenityutils/src/error.rs index 17fdd97..5df6e83 100644 --- a/bot-serenityutils/src/error.rs +++ b/bot-serenityutils/src/error.rs @@ -12,4 +12,7 @@ pub enum SerenityUtilsError { #[error("Serenity Utils not fully initialized")] Uninitialized, + + #[error("{0}")] + Msg(String), } diff --git a/bot-serenityutils/src/menu/menu.rs b/bot-serenityutils/src/menu/menu.rs index 0dca9c5..7f9fc23 100644 --- a/bot-serenityutils/src/menu/menu.rs +++ b/bot-serenityutils/src/menu/menu.rs @@ -116,6 +116,21 @@ impl Menu<'_> { &serde_json::to_value(current_page.0).unwrap(), ) .await?; + let mut controls = self + .controls + .clone() + .into_iter() + .collect::>(); + controls.sort_by_key(|(_, a)| a.position); + + for emoji in controls.into_iter().map(|(e, _)| e) { + http.create_reaction( + message.channel_id.0, + message.id.0, + &ReactionType::Unicode(emoji.clone()), + ) + .await?; + } log::trace!("New message is {:?}", message); handle.message_id = message.id.0; diff --git a/src/commands/music/current.rs b/src/commands/music/current.rs index 355fbe0..6ce5191 100644 --- a/src/commands/music/current.rs +++ b/src/commands/music/current.rs @@ -25,7 +25,8 @@ async fn current(ctx: &Context, msg: &Message) -> CommandResult { if let Some(current) = queue_lock.current() { let metadata = current.metadata().clone(); log::trace!("Metadata is {:?}", metadata); - let np_msg = create_now_playing_msg(ctx, msg.channel_id, &metadata).await?; + let np_msg = + create_now_playing_msg(ctx, msg.channel_id, &metadata, queue_lock.paused()).await?; if let Some(old_np) = mem::replace(&mut queue_lock.now_playing_msg, Some(np_msg)) { let old_np = old_np.read().await; diff --git a/src/commands/music/join.rs b/src/commands/music/join.rs index 9301c03..682aa04 100644 --- a/src/commands/music/join.rs +++ b/src/commands/music/join.rs @@ -1,19 +1,24 @@ use serenity::client::Context; use serenity::framework::standard::macros::command; -use serenity::framework::standard::CommandResult; +use serenity::framework::standard::{Args, CommandResult}; use serenity::model::channel::Message; use crate::commands::common::handle_autodelete; use crate::commands::music::{get_channel_for_author, join_channel}; +use serenity::model::id::ChannelId; #[command] #[only_in(guilds)] #[description("Joins a voice channel")] #[usage("")] #[bucket("general")] -async fn join(ctx: &Context, msg: &Message) -> CommandResult { +async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); - let channel_id = get_channel_for_author(&msg.author.id, &guild)?; + let channel_id = if let Ok(arg) = args.single::() { + ChannelId(arg) + } else { + get_channel_for_author(&msg.author.id, &guild)? + }; log::debug!("Joining channel {} for guild {}", channel_id, guild.id); join_channel(ctx, channel_id, guild.id).await; handle_autodelete(ctx, msg).await?; diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index ef04a8f..c3a4a42 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -192,7 +192,7 @@ fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult Arc { +pub async fn get_voice_manager(ctx: &Context) -> Arc { songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") @@ -252,7 +252,7 @@ async fn play_next_in_queue( log::trace!("Track is {:?}", track); if let Some(np) = &queue_lock.now_playing_msg { - if let Err(e) = update_now_playing_msg(http, np, track.metadata()).await { + if let Err(e) = update_now_playing_msg(http, np, track.metadata(), false).await { log::error!("Failed to update now playing message: {:?}", e); } } @@ -381,7 +381,7 @@ async fn added_multiple_msg(ctx: &Context, msg: &Message, songs: &mut Vec) /// Returns if the given user is a dj in the given guild based on the /// setting for the name of the dj role -async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult { +pub async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult { let dj_role = get_setting::(ctx, guild, Setting::MusicDjRole).await?; if let Some(role_name) = dj_role { diff --git a/src/commands/music/pause.rs b/src/commands/music/pause.rs index c3fbfe6..e98820e 100644 --- a/src/commands/music/pause.rs +++ b/src/commands/music/pause.rs @@ -5,6 +5,7 @@ use serenity::prelude::*; use crate::commands::common::handle_autodelete; use crate::commands::music::{get_queue_for_guild, is_dj}; +use crate::messages::music::update_now_playing_msg; #[command] #[only_in(guilds)] @@ -27,9 +28,17 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult { if queue_lock.paused() { log::debug!("Paused"); msg.channel_id.say(ctx, "Paused playback").await?; + if let (Some(menu), Some(current)) = (&queue_lock.now_playing_msg, queue_lock.current()) + { + update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?; + } } else { log::debug!("Resumed"); msg.channel_id.say(ctx, "Resumed playback").await?; + if let (Some(menu), Some(current)) = (&queue_lock.now_playing_msg, queue_lock.current()) + { + update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?; + } } } else { msg.channel_id.say(ctx, "Nothing to pause").await?; diff --git a/src/commands/music/play_next.rs b/src/commands/music/play_next.rs index 23825ad..b06bb43 100644 --- a/src/commands/music/play_next.rs +++ b/src/commands/music/play_next.rs @@ -14,7 +14,7 @@ use crate::commands::music::{ #[description("Puts a song as the next to play in the queue")] #[usage("")] #[min_args(1)] -#[aliases("pn", "play-next")] +#[aliases("pn", "play-next", "playnext")] #[bucket("music_api")] async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let query = args.message(); diff --git a/src/messages/music.rs b/src/messages/music.rs index 0bb9f3c..448517e 100644 --- a/src/messages/music.rs +++ b/src/messages/music.rs @@ -5,25 +5,42 @@ use serenity::http::Http; use serenity::model::prelude::ChannelId; use songbird::input::Metadata; -use crate::utils::error::BotResult; +use crate::commands::music::{get_queue_for_guild, get_voice_manager, is_dj}; +use crate::utils::error::*; use bot_serenityutils::core::MessageHandle; -use bot_serenityutils::menu::MenuBuilder; +use bot_serenityutils::error::SerenityUtilsResult; +use bot_serenityutils::menu::{Menu, MenuBuilder}; use serenity::builder::CreateMessage; use serenity::client::Context; +use serenity::model::channel::Reaction; use std::time::Duration; use tokio::sync::RwLock; +static PAUSE_BUTTON: &str = "⏯️"; +static SKIP_BUTTON: &str = "⏭️"; +static STOP_BUTTON: &str = "⏹️"; + /// Creates a new now playing message and returns the embed for that message pub async fn create_now_playing_msg( ctx: &Context, channel_id: ChannelId, meta: &Metadata, + paused: bool, ) -> BotResult>> { log::debug!("Creating now playing message"); let mut page = CreateMessage::default(); - page.embed(|e| create_now_playing_embed(meta, e)); + page.embed(|e| create_now_playing_embed(meta, e, paused)); let handle = MenuBuilder::default() + .add_control(0, STOP_BUTTON, |c, m, r| { + Box::pin(stop_button_action(c, m, r)) + }) + .add_control(1, PAUSE_BUTTON, |c, m, r| { + Box::pin(play_pause_button_action(c, m, r)) + }) + .add_control(2, SKIP_BUTTON, |c, m, r| { + Box::pin(skip_button_action(c, m, r)) + }) .add_page(page) .sticky(true) .timeout(Duration::from_secs(60 * 60 * 24)) @@ -38,12 +55,15 @@ pub async fn update_now_playing_msg( http: &Arc, handle: &Arc>, meta: &Metadata, + paused: bool, ) -> BotResult<()> { log::debug!("Updating now playing message"); let handle = handle.read().await; let mut message = handle.get_message(http).await?; message - .edit(http, |m| m.embed(|e| create_now_playing_embed(meta, e))) + .edit(http, |m| { + m.embed(|e| create_now_playing_embed(meta, e, paused)) + }) .await?; Ok(()) @@ -53,13 +73,16 @@ pub async fn update_now_playing_msg( fn create_now_playing_embed<'a>( meta: &Metadata, mut embed: &'a mut CreateEmbed, + paused: bool, ) -> &'a mut CreateEmbed { - embed = embed.description(format!( - "Now Playing [{}]({}) by {}", - meta.title.clone().unwrap(), - meta.source_url.clone().unwrap(), - meta.artist.clone().unwrap() - )); + embed = embed + .title(if paused { "Paused" } else { "Playing" }) + .description(format!( + "[{}]({}) by {}", + meta.title.clone().unwrap(), + meta.source_url.clone().unwrap(), + meta.artist.clone().unwrap() + )); if let Some(thumb) = meta.thumbnail.clone() { embed = embed.thumbnail(thumb); @@ -67,3 +90,98 @@ fn create_now_playing_embed<'a>( embed } + +/// Toggled when the pause button is pressed +async fn play_pause_button_action( + ctx: &Context, + _: &mut Menu<'_>, + reaction: Reaction, +) -> SerenityUtilsResult<()> { + let guild_id = reaction.guild_id.unwrap(); + let user = reaction.user(&ctx).await?; + + if !is_dj(ctx, guild_id, &user).await? { + return Ok(()); + } + { + let queue = get_queue_for_guild(ctx, &guild_id).await?; + let mut queue = queue.lock().await; + queue.pause(); + let message = queue.now_playing_msg.clone().unwrap(); + + if let Some(current) = queue.current() { + update_now_playing_msg(&ctx.http, &message, current.metadata(), queue.paused()).await?; + } + } + + Ok(()) +} + +/// Triggered when the skip button is pressed +async fn skip_button_action( + ctx: &Context, + _: &mut Menu<'_>, + reaction: Reaction, +) -> SerenityUtilsResult<()> { + let guild_id = reaction.guild_id.unwrap(); + let user = reaction.user(&ctx).await?; + + if !is_dj(ctx, guild_id, &user).await? { + return Ok(()); + } + { + let queue = get_queue_for_guild(ctx, &guild_id).await?; + let queue = queue.lock().await; + + if let Some(current) = queue.current() { + let _ = current.stop(); + } + } + + Ok(()) +} + +/// Triggered when the stop button is pressed +async fn stop_button_action( + ctx: &Context, + menu: &mut Menu<'_>, + reaction: Reaction, +) -> SerenityUtilsResult<()> { + let guild_id = reaction.guild_id.unwrap(); + let user = reaction.user(&ctx).await?; + + if !is_dj(ctx, guild_id, &user).await? { + return Ok(()); + } + { + let manager = get_voice_manager(ctx).await; + let queue = get_queue_for_guild(ctx, &guild_id).await?; + let queue = queue.lock().await; + + let handler = manager.get(guild_id); + + if let Some(handler) = handler { + let mut handler_lock = handler.lock().await; + handler_lock.remove_all_global_events(); + } + if let Some(current) = queue.current() { + current.stop().map_err(BotError::from)?; + } + + if manager.get(guild_id).is_some() { + manager.remove(guild_id).await.map_err(BotError::from)?; + log::debug!("Left the voice channel"); + } else { + log::debug!("Not in a voice channel"); + } + } + { + let handle = &menu.message; + let handle = handle.read().await; + ctx.http + .delete_message(handle.channel_id, handle.message_id) + .await?; + } + + Ok(()) +} diff --git a/src/utils/error.rs b/src/utils/error.rs index 61ef287..487ae43 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -1,3 +1,4 @@ +use bot_serenityutils::error::SerenityUtilsError; use thiserror::Error; pub type BotResult = Result; @@ -34,6 +35,12 @@ pub enum BotError { #[error("Serenity Utils Error: {0}")] SerenityUtils(#[from] bot_serenityutils::error::SerenityUtilsError), + #[error("Track Error: {0}")] + TrackError(#[from] songbird::error::TrackError), + + #[error("JoinError: {0}")] + JoinError(#[from] songbird::error::JoinError), + #[error("{0}")] Msg(String), } @@ -43,3 +50,9 @@ impl From<&str> for BotError { Self::Msg(s.to_string()) } } + +impl From for SerenityUtilsError { + fn from(e: BotError) -> Self { + Self::Msg(format!("{:?}", e)) + } +}