diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index ad01fd5..d5dd669 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -41,7 +41,7 @@ jobs: - name: Move binaries run: mv target/x86_64-unknown-linux-gnu/release/tobi-rs target/tobi-rs-linux-x86_64 - name: Sign artifact - run: gpg --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64.sig target/tobi-rs-linux-x86_64 + run: gpg --batch --yes --pinentry-mode loopback --passphrase "${{ secrets.PASSPHRASE }}" --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64.sig target/tobi-rs-linux-x86_64 - name: Upload artifacts uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a54b9c..f9ef2e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,7 +55,7 @@ jobs: passphrase: ${{ secrets.PASSPHRASE }} - name: Sign artifact - run: gpg --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64_debug.sig target/tobi-rs-linux-x86_64_debug + run: gpg --batch --yes --pinentry-mode loopback --passphrase "${{ secrets.PASSPHRASE }}" --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64_debug.sig target/tobi-rs-linux-x86_64_debug - name: Upload artifacts uses: actions/upload-artifact@v2 diff --git a/Cargo.lock b/Cargo.lock index 67403fe..9496281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,7 +207,7 @@ dependencies = [ [[package]] name = "bot-serenityutils" -version = "0.2.1" +version = "0.2.2" dependencies = [ "futures", "log 0.4.14", @@ -2318,7 +2318,7 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tobi-rs" -version = "0.6.2" +version = "0.6.3" dependencies = [ "aspotify", "bot-coreutils", @@ -2347,6 +2347,7 @@ dependencies = [ "thiserror", "tokio", "trigram", + "typemap_rev", ] [[package]] @@ -2535,9 +2536,9 @@ dependencies = [ [[package]] name = "typemap_rev" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335fb14412163adc9ed4a3e53335afaa7a4b72bdd122e5f72f51b8f1db1a131e" +checksum = "ed5b74f0a24b5454580a79abb6994393b09adf0ab8070f15827cb666255de155" [[package]] name = "typenum" diff --git a/Cargo.toml b/Cargo.toml index 9f2a3fc..5d86e9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tobi-rs" -version = "0.6.2" +version = "0.6.3" authors = ["trivernis "] edition = "2018" @@ -36,4 +36,5 @@ reqwest = "0.11.2" chrono-tz = "0.5.3" sauce-api = "0.7.1" rustc_version_runtime = "0.2.0" -trigram = "0.4.4" \ No newline at end of file +trigram = "0.4.4" +typemap_rev = "0.1.5" \ No newline at end of file diff --git a/bot-serenityutils/Cargo.toml b/bot-serenityutils/Cargo.toml index 56f899f..fbebef9 100644 --- a/bot-serenityutils/Cargo.toml +++ b/bot-serenityutils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bot-serenityutils" -version = "0.2.1" +version = "0.2.2" authors = ["trivernis "] edition = "2018" diff --git a/bot-serenityutils/src/menu/controls.rs b/bot-serenityutils/src/menu/controls.rs index c76888d..a69c2ee 100644 --- a/bot-serenityutils/src/menu/controls.rs +++ b/bot-serenityutils/src/menu/controls.rs @@ -1,9 +1,14 @@ use crate::error::{SerenityUtilsError, SerenityUtilsResult}; use crate::menu::container::get_listeners_from_context; use crate::menu::menu::Menu; +use crate::menu::typedata::HelpActiveContainer; +use crate::menu::ActionContainer; +use serde_json::json; +use serde_json::Value; use serenity::client::Context; use serenity::http::CacheHttp; use serenity::model::channel::Reaction; +use std::sync::atomic::Ordering; /// Shows the next page in the menu pub async fn next_page(ctx: &Context, menu: &mut Menu<'_>, _: Reaction) -> SerenityUtilsResult<()> { @@ -47,6 +52,77 @@ pub async fn close_menu( Ok(()) } +pub async fn toggle_help( + ctx: &Context, + menu: &mut Menu<'_>, + _: Reaction, +) -> SerenityUtilsResult<()> { + log::debug!("Displaying help"); + let show_help = menu + .data + .get::() + .expect("Missing HelpActiveContainer in menu data") + .clone(); + + if show_help.load(Ordering::Relaxed) { + display_page(ctx, menu).await?; + show_help.store(false, Ordering::Relaxed); + return Ok(()); + } + let page = menu + .pages + .get(menu.current_page) + .ok_or(SerenityUtilsError::PageNotFound(menu.current_page))? + .get() + .await?; + let mut message = menu.get_message(ctx.http()).await?; + log::debug!("Building help entries"); + let mut help_entries = menu + .help_entries + .iter() + .filter_map(|(e, h)| Some((menu.controls.get(e)?, e, h))) + .collect::>(); + help_entries.sort_by_key(|(c, _, _)| c.position()); + let help_message = help_entries + .into_iter() + .map(|(_, e, h)| format!(" - {} {}", e, h)) + .collect::>() + .join("\n"); + log::trace!("Help message is {}", help_message); + + message + .edit(ctx, |m| { + m.0.clone_from(&mut page.0.clone()); + + if let Some(embed) = m.0.get_mut("embed") { + let embed = embed.as_object_mut().unwrap(); + let fields = embed + .entry("fields") + .or_insert_with(|| Value::Array(vec![])); + if let Value::Array(ref mut inner) = *fields { + inner.push(json!({ + "inline": false, + "name": "Help".to_string(), + "value": help_message, + })); + } + } else { + m.embed(|e| { + e.field("Help", help_message, false); + + e + }); + } + + m + }) + .await?; + log::debug!("Help message displayed"); + show_help.store(true, Ordering::Relaxed); + + Ok(()) +} + /// Displays the menu page async fn display_page(ctx: &Context, menu: &mut Menu<'_>) -> SerenityUtilsResult<()> { log::debug!("Displaying page {}", menu.current_page); diff --git a/bot-serenityutils/src/menu/menu.rs b/bot-serenityutils/src/menu/menu.rs index b874b8c..24fd310 100644 --- a/bot-serenityutils/src/menu/menu.rs +++ b/bot-serenityutils/src/menu/menu.rs @@ -1,8 +1,9 @@ use crate::core::MessageHandle; use crate::error::{SerenityUtilsError, SerenityUtilsResult}; use crate::menu::container::get_listeners_from_context; -use crate::menu::controls::{close_menu, next_page, previous_page}; +use crate::menu::controls::{close_menu, next_page, previous_page, toggle_help}; use crate::menu::traits::EventDrivenMessage; +use crate::menu::typedata::HelpActiveContainer; use crate::menu::{EventDrivenMessagesRef, Page}; use futures::FutureExt; use serenity::async_trait; @@ -10,9 +11,11 @@ use serenity::client::Context; use serenity::http::Http; use serenity::model::channel::{Message, Reaction, ReactionType}; use serenity::model::id::ChannelId; +use serenity::prelude::{TypeMap, TypeMapKey}; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; +use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{Mutex, RwLock}; @@ -20,6 +23,7 @@ use tokio::sync::{Mutex, RwLock}; pub static NEXT_PAGE_EMOJI: &str = "➡️"; pub static PREVIOUS_PAGE_EMOJI: &str = "⬅️"; pub static CLOSE_MENU_EMOJI: &str = "❌"; +pub static HELP_EMOJI: &str = "❔"; pub type ControlActionResult<'b> = Pin> + Send + 'b>>; @@ -33,12 +37,12 @@ pub type ControlActionArc = Arc< #[derive(Clone)] pub struct ActionContainer { inner: ControlActionArc, - position: usize, + position: isize, } impl ActionContainer { /// Creates a new control action - pub fn new(position: usize, callback: F) -> Self + pub fn new(position: isize, callback: F) -> Self where F: for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b> + Send @@ -60,10 +64,14 @@ impl ActionContainer { self.inner.clone()(ctx, menu, reaction).await?; Ok(()) } + + /// Returns the position of the action + pub fn position(&self) -> isize { + self.position + } } /// A menu message -#[derive(Clone)] pub struct Menu<'a> { pub message: Arc>, pub pages: Vec>, @@ -71,6 +79,8 @@ pub struct Menu<'a> { pub controls: HashMap, pub timeout: Instant, pub sticky: bool, + pub data: TypeMap, + pub help_entries: HashMap, closed: bool, listeners: EventDrivenMessagesRef, } @@ -221,6 +231,8 @@ pub struct MenuBuilder { controls: HashMap, timeout: Duration, sticky: bool, + data: TypeMap, + help_entries: HashMap, } impl Default for MenuBuilder { @@ -231,6 +243,8 @@ impl Default for MenuBuilder { controls: HashMap::new(), timeout: Duration::from_secs(60), sticky: false, + data: TypeMap::new(), + help_entries: HashMap::new(), } } } @@ -240,21 +254,35 @@ impl MenuBuilder { pub fn new_paginator() -> Self { log::debug!("Creating new paginator"); let mut controls = HashMap::new(); + let mut help_entries = HashMap::new(); controls.insert( PREVIOUS_PAGE_EMOJI.to_string(), ActionContainer::new(0, |c, m, r| previous_page(c, m, r).boxed()), ); + help_entries.insert( + PREVIOUS_PAGE_EMOJI.to_string(), + "Displays the previous page".to_string(), + ); controls.insert( CLOSE_MENU_EMOJI.to_string(), ActionContainer::new(1, |c, m, r| close_menu(c, m, r).boxed()), ); + help_entries.insert( + CLOSE_MENU_EMOJI.to_string(), + "Closes the menu buttons".to_string(), + ); controls.insert( NEXT_PAGE_EMOJI.to_string(), ActionContainer::new(2, |c, m, r| next_page(c, m, r).boxed()), ); + help_entries.insert( + NEXT_PAGE_EMOJI.to_string(), + "Displays the next page".to_string(), + ); Self { controls, + help_entries, ..Default::default() } } @@ -278,7 +306,7 @@ impl MenuBuilder { } /// Adds a single control to the message - pub fn add_control(mut self, position: usize, emoji: S, action: F) -> Self + pub fn add_control(mut self, position: isize, emoji: S, action: F) -> Self where S: ToString, F: for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b> @@ -295,7 +323,7 @@ impl MenuBuilder { pub fn add_controls(mut self, controls: I) -> Self where S: ToString, - I: IntoIterator, + I: IntoIterator, { for (position, emoji, action) in controls { self.controls.insert( @@ -332,6 +360,30 @@ impl MenuBuilder { self } + /// Adds data to the menu typemap + pub fn add_data(mut self, value: T::Value) -> Self + where + T: TypeMapKey, + { + self.data.insert::(value); + + self + } + + /// Adds a help entry + pub fn add_help(mut self, button: S, help: S) -> Self { + self.help_entries + .insert(button.to_string(), help.to_string()); + + self + } + + /// Turns showing help for buttons on + pub fn show_help(self) -> Self { + self.add_control(100, HELP_EMOJI, |c, m, r| Box::pin(toggle_help(c, m, r))) + .add_data::(Arc::new(AtomicBool::new(false))) + } + /// builds the menu pub async fn build( self, @@ -371,6 +423,8 @@ impl MenuBuilder { closed: false, listeners: Arc::clone(&listeners), sticky: self.sticky, + data: self.data, + help_entries: self.help_entries, }; log::debug!("Storing menu to listeners..."); diff --git a/bot-serenityutils/src/menu/mod.rs b/bot-serenityutils/src/menu/mod.rs index 8eaffaf..f38518b 100644 --- a/bot-serenityutils/src/menu/mod.rs +++ b/bot-serenityutils/src/menu/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod controls; pub(crate) mod menu; pub(crate) mod page; pub(crate) mod traits; +pub(crate) mod typedata; pub use container::*; pub use controls::*; diff --git a/bot-serenityutils/src/menu/typedata.rs b/bot-serenityutils/src/menu/typedata.rs new file mode 100644 index 0000000..c2d6a59 --- /dev/null +++ b/bot-serenityutils/src/menu/typedata.rs @@ -0,0 +1,9 @@ +use serenity::prelude::TypeMapKey; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +pub struct HelpActiveContainer; + +impl TypeMapKey for HelpActiveContainer { + type Value = Arc; +} diff --git a/src/commands/music/play.rs b/src/commands/music/play.rs index 99a6fa4..2664018 100644 --- a/src/commands/music/play.rs +++ b/src/commands/music/play.rs @@ -8,6 +8,7 @@ use crate::commands::music::{ get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager, join_channel, play_next_in_queue, }; +use crate::messages::music::now_playing::create_now_playing_msg; use crate::providers::settings::{get_setting, Setting}; #[command] @@ -43,7 +44,7 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let queue = get_queue_for_guild(ctx, &guild.id).await?; - let play_first = { + let (play_first, create_now_playing) = { log::debug!("Adding song to queue"); let mut queue_lock = queue.lock().await; for song in songs { @@ -57,13 +58,19 @@ async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { log::debug!("Autoshuffeling"); queue_lock.shuffle(); } - queue_lock.current().is_none() + ( + queue_lock.current().is_none(), + queue_lock.now_playing_msg.is_none(), + ) }; if play_first { log::debug!("Playing first song in queue"); while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler_lock).await {} } + if create_now_playing { + create_now_playing_msg(ctx, queue, msg.channel_id).await?; + } handle_autodelete(ctx, msg).await?; Ok(()) diff --git a/src/commands/music/play_next.rs b/src/commands/music/play_next.rs index bfdcf64..c4d3b45 100644 --- a/src/commands/music/play_next.rs +++ b/src/commands/music/play_next.rs @@ -8,6 +8,7 @@ use crate::commands::music::{ get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager, join_channel, play_next_in_queue, DJ_CHECK, }; +use crate::messages::music::now_playing::create_now_playing_msg; #[command] #[only_in(guilds)] @@ -41,7 +42,7 @@ async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let mut songs = get_songs_for_query(&ctx, msg, query).await?; let queue = get_queue_for_guild(ctx, &guild.id).await?; - let play_first = { + let (play_first, create_now_playing) = { let mut queue_lock = queue.lock().await; songs.reverse(); log::debug!("Enqueueing songs as next songs in the queue"); @@ -49,12 +50,18 @@ async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult { for song in songs { queue_lock.add_next(song); } - queue_lock.current().is_none() + ( + queue_lock.current().is_none(), + queue_lock.now_playing_msg.is_none(), + ) }; if play_first { while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler).await {} } + if create_now_playing { + create_now_playing_msg(ctx, queue, msg.channel_id).await?; + } handle_autodelete(ctx, msg).await?; Ok(()) diff --git a/src/handler.rs b/src/handler.rs index 8c7bbb7..5bd3da4 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -120,10 +120,11 @@ impl EventHandler for Handler { if let Some(count) = member_count { log::debug!("{} Members in channel", count); - let queue = get_queue_for_guild(&ctx, &guild_id).await.unwrap(); - let mut queue_lock = queue.lock().await; - log::debug!("Setting leave flag to {}", count == 0); - queue_lock.leave_flag = count == 0; + if let Ok(queue) = get_queue_for_guild(&ctx, &guild_id).await { + let mut queue_lock = queue.lock().await; + log::debug!("Setting leave flag to {}", count == 0); + queue_lock.leave_flag = count == 0; + } } } } diff --git a/src/messages/gifs.rs b/src/messages/gifs.rs index cf832d3..788f91d 100644 --- a/src/messages/gifs.rs +++ b/src/messages/gifs.rs @@ -22,6 +22,7 @@ pub async fn create_gifs_menu( MenuBuilder::new_paginator() .timeout(Duration::from_secs(120)) .add_pages(pages) + .show_help() .build(ctx, channel_id) .await?; diff --git a/src/messages/music/now_playing.rs b/src/messages/music/now_playing.rs index 508f092..3f37bed 100644 --- a/src/messages/music/now_playing.rs +++ b/src/messages/music/now_playing.rs @@ -21,6 +21,7 @@ use std::env; use std::time::Duration; use tokio::sync::{Mutex, RwLock}; +static DELETE_BUTTON: &str = "🗑️"; static PAUSE_BUTTON: &str = "⏯️"; static SKIP_BUTTON: &str = "⏭️"; static STOP_BUTTON: &str = "⏹️"; @@ -34,18 +35,30 @@ pub async fn create_now_playing_msg( ) -> BotResult>> { log::debug!("Creating now playing menu"); let handle = MenuBuilder::default() + .add_control(-1, DELETE_BUTTON, |c, m, r| { + Box::pin(delete_action(c, m, r)) + }) + .add_help(DELETE_BUTTON, "Deletes this message") .add_control(0, STOP_BUTTON, |c, m, r| { Box::pin(stop_button_action(c, m, r)) }) + .add_help(STOP_BUTTON, "Stops the music and leaves the channel") .add_control(1, PAUSE_BUTTON, |c, m, r| { Box::pin(play_pause_button_action(c, m, r)) }) + .add_help(PAUSE_BUTTON, "Pauses the music") .add_control(2, SKIP_BUTTON, |c, m, r| { Box::pin(skip_button_action(c, m, r)) }) + .add_help(SKIP_BUTTON, "Skips to the next song") .add_control(3, GOOD_PICK_BUTTON, |c, m, r| { Box::pin(good_pick_action(c, m, r)) }) + .add_help( + GOOD_PICK_BUTTON, + "Remembers this video for spotify-youtube mappings", + ) + .show_help() .add_page(Page::new_builder(move || { let queue = Arc::clone(&queue); Box::pin(async move { @@ -249,3 +262,25 @@ async fn good_pick_action( Ok(()) } + +async fn delete_action( + ctx: &Context, + menu: &mut Menu<'_>, + reaction: Reaction, +) -> SerenityUtilsResult<()> { + let guild_id = reaction.guild_id.unwrap(); + let handle = { + let handle = menu.message.read().await; + handle.clone() + }; + { + let queue = get_queue_for_guild(ctx, &guild_id).await?; + let mut queue = queue.lock().await; + queue.now_playing_msg = None; + } + ctx.http + .delete_message(handle.channel_id, handle.message_id) + .await?; + + Ok(()) +} diff --git a/src/messages/sauce.rs b/src/messages/sauce.rs index e0aeb08..527ae24 100644 --- a/src/messages/sauce.rs +++ b/src/messages/sauce.rs @@ -38,6 +38,7 @@ pub async fn show_sauce_menu( MenuBuilder::new_paginator() .timeout(Duration::from_secs(600)) .add_pages(pages) + .show_help() .build(ctx, msg.channel_id) .await?; }