diff --git a/src/commands/music/current.rs b/src/commands/music/current.rs index 610c760..535790b 100644 --- a/src/commands/music/current.rs +++ b/src/commands/music/current.rs @@ -4,6 +4,8 @@ use serenity::framework::standard::CommandResult; use serenity::model::channel::Message; use crate::commands::music::get_queue_for_guild; +use crate::messages::music::NowPlayingMessage; +use std::mem; #[command] #[only_in(guilds)] @@ -15,29 +17,17 @@ async fn current(ctx: &Context, msg: &Message) -> CommandResult { log::debug!("Displaying current song for queue in {}", guild.id); let queue = get_queue_for_guild(ctx, &guild.id).await?; - let queue_lock = queue.lock().await; + let mut queue_lock = queue.lock().await; if let Some(current) = queue_lock.current() { let metadata = current.metadata().clone(); log::trace!("Metadata is {:?}", metadata); - msg.channel_id - .send_message(ctx, |m| { - m.embed(|mut e| { - e = e.description(format!( - "Now Playing [{}]({}) by {}", - metadata.title.unwrap(), - metadata.source_url.unwrap(), - metadata.artist.unwrap() - )); + let np_msg = + NowPlayingMessage::create(ctx.http.clone(), &msg.channel_id, &metadata).await?; - if let Some(thumb) = metadata.thumbnail { - e = e.thumbnail(thumb); - } - - e - }) - }) - .await?; + if let Some(old_np) = mem::replace(&mut queue_lock.now_playing_msg, Some(np_msg)) { + let _ = old_np.inner().delete().await; + } } Ok(()) diff --git a/src/commands/music/leave.rs b/src/commands/music/leave.rs index e305e2b..cea6277 100644 --- a/src/commands/music/leave.rs +++ b/src/commands/music/leave.rs @@ -13,12 +13,10 @@ use crate::commands::music::{get_queue_for_guild, get_voice_manager}; #[allowed_roles("DJ")] async fn leave(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).await.unwrap(); - log::debug!("Leave request received for guild {}", guild.id); let manager = get_voice_manager(ctx).await; let queue = get_queue_for_guild(ctx, &guild.id).await?; let queue_lock = queue.lock().await; - log::trace!("Queue is {:?}", queue_lock); let handler = manager.get(guild.id); if let Some(handler) = handler { diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index e48bab4..4db3e0b 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -1,5 +1,12 @@ use std::sync::Arc; +use crate::providers::music::queue::{MusicQueue, Song}; +use crate::providers::music::{ + get_video_information, get_videos_for_playlist, search_video_information, +}; +use crate::utils::context_data::{DatabaseContainer, Store}; +use crate::utils::error::{BotError, BotResult}; +use regex::Regex; use serenity::async_trait; use serenity::client::Context; use serenity::framework::standard::macros::group; @@ -10,31 +17,10 @@ use serenity::model::id::{ChannelId, GuildId, UserId}; use songbird::{ Call, Event, EventContext, EventHandler as VoiceEventHandler, Songbird, TrackEvent, }; -use tokio::sync::Mutex; - -use clear_queue::CLEAR_QUEUE_COMMAND; -use current::CURRENT_COMMAND; -use join::JOIN_COMMAND; -use leave::LEAVE_COMMAND; -use lyrics::LYRICS_COMMAND; -use pause::PAUSE_COMMAND; -use play::PLAY_COMMAND; -use play_next::PLAY_NEXT_COMMAND; -use playlists::PLAYLISTS_COMMAND; -use queue::QUEUE_COMMAND; -use save_playlist::SAVE_PLAYLIST_COMMAND; -use shuffle::SHUFFLE_COMMAND; -use skip::SKIP_COMMAND; - -use crate::providers::music::queue::{MusicQueue, Song}; -use crate::providers::music::{ - get_video_information, get_videos_for_playlist, search_video_information, -}; -use crate::utils::context_data::{DatabaseContainer, Store}; -use crate::utils::error::{BotError, BotResult}; -use regex::Regex; +use std::mem; use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering}; use std::time::Duration; +use tokio::sync::Mutex; mod clear_queue; mod current; @@ -50,6 +36,20 @@ mod save_playlist; mod shuffle; mod skip; +use clear_queue::CLEAR_QUEUE_COMMAND; +use current::CURRENT_COMMAND; +use join::JOIN_COMMAND; +use leave::LEAVE_COMMAND; +use lyrics::LYRICS_COMMAND; +use pause::PAUSE_COMMAND; +use play::PLAY_COMMAND; +use play_next::PLAY_NEXT_COMMAND; +use playlists::PLAYLISTS_COMMAND; +use queue::QUEUE_COMMAND; +use save_playlist::SAVE_PLAYLIST_COMMAND; +use shuffle::SHUFFLE_COMMAND; +use skip::SKIP_COMMAND; + #[group] #[commands( join, @@ -248,8 +248,15 @@ async fn play_next_in_queue( let mut handler_lock = handler.lock().await; let track = handler_lock.play_only_source(source); log::trace!("Track is {:?}", track); + + if let Some(np) = &queue_lock.now_playing_msg { + let _ = np.refresh(track.metadata()).await; + } queue_lock.set_current(track); } else { + if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) { + let _ = np.inner().delete().await; + } queue_lock.clear_current(); } true diff --git a/src/commands/music/play.rs b/src/commands/music/play.rs index 51496f7..fa2562a 100644 --- a/src/commands/music/play.rs +++ b/src/commands/music/play.rs @@ -8,7 +8,7 @@ use crate::commands::music::{ join_channel, play_next_in_queue, }; -use crate::providers::constants::SETTING_AUTOSHUFFLE; +use crate::commands::settings::SETTING_AUTOSHUFFLE; use crate::utils::context_data::get_database_from_context; #[command] diff --git a/src/commands/settings/get.rs b/src/commands/settings/get.rs index 971a357..b077278 100644 --- a/src/commands/settings/get.rs +++ b/src/commands/settings/get.rs @@ -3,7 +3,7 @@ use serenity::framework::standard::macros::command; use serenity::framework::standard::{Args, CommandResult}; use serenity::model::channel::Message; -use crate::providers::constants::GUILD_SETTINGS; +use crate::commands::settings::GUILD_SETTINGS; use crate::utils::context_data::get_database_from_context; #[command] diff --git a/src/commands/settings/mod.rs b/src/commands/settings/mod.rs index fc894df..7215deb 100644 --- a/src/commands/settings/mod.rs +++ b/src/commands/settings/mod.rs @@ -10,3 +10,6 @@ mod set; #[commands(set, get)] #[prefix("settings")] pub struct Settings; + +pub const SETTING_AUTOSHUFFLE: &str = "music.autoshuffle"; +pub const GUILD_SETTINGS: &[&str] = &[SETTING_AUTOSHUFFLE]; diff --git a/src/commands/settings/set.rs b/src/commands/settings/set.rs index 5d4a7e7..436e96c 100644 --- a/src/commands/settings/set.rs +++ b/src/commands/settings/set.rs @@ -3,7 +3,7 @@ use serenity::framework::standard::macros::command; use serenity::framework::standard::{Args, CommandResult}; use serenity::model::channel::Message; -use crate::providers::constants::GUILD_SETTINGS; +use crate::commands::settings::GUILD_SETTINGS; use crate::utils::context_data::get_database_from_context; #[command] diff --git a/src/handler.rs b/src/handler.rs index 247b8f8..5eca697 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,15 +1,25 @@ use crate::commands::music::get_queue_for_guild; +use crate::utils::context_data::EventDrivenMessageContainer; use serenity::async_trait; use serenity::client::Context; +use serenity::model::channel::Reaction; use serenity::model::event::ResumedEvent; use serenity::model::gateway::{Activity, Ready}; use serenity::model::guild::Member; -use serenity::model::id::{ChannelId, GuildId}; +use serenity::model::id::{ChannelId, GuildId, MessageId}; use serenity::model::voice::VoiceState; use serenity::prelude::*; pub(crate) struct Handler; +macro_rules! log_msg_fire_error { + ($msg:expr) => { + if let Err(e) = $msg { + log::error!("Failed to handle event for message: {:?}", e); + } + }; +} + #[async_trait] impl EventHandler for Handler { async fn ready(&self, ctx: Context, ready: Ready) { @@ -55,6 +65,67 @@ impl EventHandler for Handler { queue_lock.leave_flag = count == 0; } } + + /// Fired when a message was deleted + async fn message_delete( + &self, + ctx: Context, + channel_id: ChannelId, + message_id: MessageId, + _: Option, + ) { + let mut data = ctx.data.write().await; + let listeners = data.get_mut::().unwrap(); + + if let Some(msg) = listeners.get(&(channel_id.0, message_id.0)) { + log_msg_fire_error!(msg.on_deleted().await); + listeners.remove(&(channel_id.0, message_id.0)); + } + } + + /// Fired when multiple messages were deleted + async fn message_delete_bulk( + &self, + ctx: Context, + channel_id: ChannelId, + message_ids: Vec, + _: Option, + ) { + let data = ctx.data.read().await; + let listeners = data.get::().unwrap(); + + for message_id in message_ids { + if let Some(msg) = listeners.get(&(channel_id.0, message_id.0)) { + log_msg_fire_error!(msg.on_deleted().await); + } + } + } + + /// Fired when a reaction was added to a message + async fn reaction_add(&self, ctx: Context, reaction: Reaction) { + let data = ctx.data.read().await; + let listeners = data.get::().unwrap(); + + let message_id = reaction.message_id; + let channel_id = reaction.channel_id; + + if let Some(msg) = listeners.get(&(channel_id.0, message_id.0)) { + log_msg_fire_error!(msg.on_reaction_add(reaction).await); + } + } + + /// Fired when a reaction was added to a message + async fn reaction_remove(&self, ctx: Context, reaction: Reaction) { + let data = ctx.data.read().await; + let listeners = data.get::().unwrap(); + + let message_id = reaction.message_id; + let channel_id = reaction.channel_id; + + if let Some(msg) = listeners.get(&(channel_id.0, message_id.0)) { + log_msg_fire_error!(msg.on_reaction_remove(reaction).await); + } + } } /// Returns the number of members in the channel if it's the bots voice channel diff --git a/src/main.rs b/src/main.rs index d74ecee..2a8aa88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use crate::utils::logging::init_logger; pub mod client; mod commands; pub mod handler; +mod messages; mod providers; pub mod utils; diff --git a/src/messages/mod.rs b/src/messages/mod.rs new file mode 100644 index 0000000..3bad0b3 --- /dev/null +++ b/src/messages/mod.rs @@ -0,0 +1 @@ +pub mod music; diff --git a/src/messages/music.rs b/src/messages/music.rs new file mode 100644 index 0000000..12e8967 --- /dev/null +++ b/src/messages/music.rs @@ -0,0 +1,58 @@ +use crate::utils::error::BotResult; +use crate::utils::messages::ShareableMessage; +use serenity::builder::CreateEmbed; +use serenity::http::Http; +use serenity::model::prelude::ChannelId; +use songbird::input::Metadata; +use std::sync::Arc; + +#[derive(Clone)] +pub struct NowPlayingMessage { + inner: ShareableMessage, +} + +impl NowPlayingMessage { + /// Creates a new now playing message + pub async fn create( + ctx: Arc, + channel_id: &ChannelId, + meta: &Metadata, + ) -> BotResult { + let inner = ShareableMessage::create(ctx, channel_id, |f| { + f.embed(|e| Self::create_embed(meta, e)) + }) + .await?; + + Ok(Self { inner }) + } + + /// Returns the inner shareable message + pub fn inner(&self) -> &ShareableMessage { + &self.inner + } + + /// Refreshes the now playing message + pub async fn refresh(&self, meta: &Metadata) -> BotResult<()> { + self.inner + .edit(|m| m.embed(|e| Self::create_embed(meta, e))) + .await?; + + Ok(()) + } + + /// Creates the embed of the now playing message + fn create_embed<'a>(meta: &Metadata, mut embed: &'a mut CreateEmbed) -> &'a mut CreateEmbed { + embed = embed.description(format!( + "Now Playing [{}]({}) 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); + } + + embed + } +} diff --git a/src/providers/constants.rs b/src/providers/constants.rs deleted file mode 100644 index a3cbbcb..0000000 --- a/src/providers/constants.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub const SETTING_AUTOSHUFFLE: &str = "music.autoshuffle"; -pub const GUILD_SETTINGS: &[&str] = &[SETTING_AUTOSHUFFLE]; diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 3dbf313..bfb8134 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1,2 +1 @@ -pub(crate) mod constants; pub(crate) mod music; diff --git a/src/providers/music/queue.rs b/src/providers/music/queue.rs index 63f60e0..712439c 100644 --- a/src/providers/music/queue.rs +++ b/src/providers/music/queue.rs @@ -2,16 +2,18 @@ use std::collections::VecDeque; use songbird::tracks::TrackHandle; +use crate::messages::music::NowPlayingMessage; use crate::providers::music::responses::{PlaylistEntry, VideoInformation}; use crate::providers::music::search_video_information; use crate::utils::shuffle_vec_deque; use aspotify::{Track, TrackSimplified}; -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct MusicQueue { inner: VecDeque, current: Option, paused: bool, + pub now_playing_msg: Option, pub leave_flag: bool, } @@ -22,6 +24,7 @@ impl MusicQueue { current: None, paused: false, leave_flag: false, + now_playing_msg: None, } } @@ -85,6 +88,7 @@ impl MusicQueue { } } + /// Returns if the queue is paused pub fn paused(&self) -> bool { self.paused } diff --git a/src/utils/context_data.rs b/src/utils/context_data.rs index 0249597..46bba6d 100644 --- a/src/utils/context_data.rs +++ b/src/utils/context_data.rs @@ -7,6 +7,7 @@ use tokio::sync::Mutex; use crate::providers::music::queue::MusicQueue; use crate::providers::music::spotify::SpotifyApi; +use crate::utils::messages::EventDrivenMessage; use database::Database; use serenity::client::Context; @@ -49,3 +50,9 @@ pub async fn get_database_from_context(ctx: &Context) -> Database { database.clone() } + +pub struct EventDrivenMessageContainer; + +impl TypeMapKey for EventDrivenMessageContainer { + type Value = HashMap<(u64, u64), Box>; +} diff --git a/src/utils/messages.rs b/src/utils/messages.rs new file mode 100644 index 0000000..d3f1b34 --- /dev/null +++ b/src/utils/messages.rs @@ -0,0 +1,72 @@ +use crate::utils::error::BotResult; +use serenity::async_trait; +use serenity::builder::{CreateMessage, EditMessage}; +use serenity::http::{CacheHttp, Http}; +use serenity::model::channel::{Message, Reaction}; +use serenity::model::id::{ChannelId, MessageId}; +use std::sync::Arc; + +#[async_trait] +pub trait EventDrivenMessage: Send + Sync { + async fn on_deleted(&self) -> BotResult<()>; + async fn on_reaction_add(&self, reaction: Reaction) -> BotResult<()>; + async fn on_reaction_remove(&self, reaction: Reaction) -> BotResult<()>; +} + +#[derive(Clone)] +pub struct ShareableMessage { + http: Arc, + channel_id: u64, + message_id: u64, +} + +impl ShareableMessage { + /// Creates a new active message + pub async fn create<'a, F>(http: Arc, channel_id: &ChannelId, f: F) -> BotResult + where + for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>, + { + let msg = channel_id.send_message(http.http(), f).await?; + + Ok(Self::new(http, &msg.channel_id, &msg.id)) + } + + /// Creates a new active message + pub fn new(http: Arc, channel_id: &ChannelId, message_id: &MessageId) -> Self { + Self { + http, + channel_id: channel_id.0, + message_id: message_id.0, + } + } + + /// Deletes the underlying message + pub async fn delete(&self) -> BotResult<()> { + let msg = self.get_message().await?; + msg.delete(&self.http).await?; + + Ok(()) + } + + /// Edits the active message + pub async fn edit(&self, f: F) -> BotResult<()> + where + F: FnOnce(&mut EditMessage) -> &mut EditMessage, + { + let mut message = self.get_message().await?; + message.edit(&self.http, f).await?; + + Ok(()) + } + + /// Returns the underlying message + async fn get_message(&self) -> BotResult { + let message = self + .http + .http() + .get_message(self.channel_id, self.message_id) + .await?; + + Ok(message) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 772523d..9467c6f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,6 +5,7 @@ use rand::Rng; pub(crate) mod context_data; pub(crate) mod error; pub(crate) mod logging; +pub(crate) mod messages; /// Fisher-Yates shuffle for VecDeque pub fn shuffle_vec_deque(deque: &mut VecDeque) {