From a8958a20f2a94e85a9b2905d264c01670fbab5fe Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Thu, 3 Sep 2020 17:55:19 -0400 Subject: [PATCH] Remove ReactionWatcher (use serenity collector!) and simplify paginate --- Cargo.lock | 1 + youmubot-prelude/Cargo.toml | 7 +- youmubot-prelude/src/lib.rs | 4 +- youmubot-prelude/src/pagination.rs | 218 +++++++++---------------- youmubot-prelude/src/reaction_watch.rs | 105 ------------ 5 files changed, 85 insertions(+), 250 deletions(-) delete mode 100644 youmubot-prelude/src/reaction_watch.rs diff --git a/Cargo.lock b/Cargo.lock index 412d9a3..3928978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1975,6 +1975,7 @@ dependencies = [ "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", "serenity 0.9.0-rc.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "youmubot-db 0.1.0", ] diff --git a/youmubot-prelude/Cargo.toml b/youmubot-prelude/Cargo.toml index 1651239..685e069 100644 --- a/youmubot-prelude/Cargo.toml +++ b/youmubot-prelude/Cargo.toml @@ -7,12 +7,17 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serenity = "0.9.0-rc.0" anyhow = "1.0" async-trait = "0.1" futures-util = "0.3" +tokio = { version = "0.2", features = ["time"] } youmubot-db = { path = "../youmubot-db" } crossbeam-channel = "0.4" reqwest = "0.10" rayon = "1" chrono = "0.4" + +[dependencies.serenity] +version = "0.9.0-rc.0" +default-features = true +features = ["collector"] diff --git a/youmubot-prelude/src/lib.rs b/youmubot-prelude/src/lib.rs index 20ad9a1..dca3b8a 100644 --- a/youmubot-prelude/src/lib.rs +++ b/youmubot-prelude/src/lib.rs @@ -4,13 +4,11 @@ use std::sync::Arc; pub mod announcer; pub mod args; pub mod pagination; -pub mod reaction_watch; pub mod setup; pub use announcer::{Announcer, AnnouncerHandler}; pub use args::{Duration, UsernameArg}; -pub use pagination::Pagination; -pub use reaction_watch::{ReactionHandler, ReactionWatcher}; +pub use pagination::paginate; /// Re-export the anyhow errors pub use anyhow::{Error, Result}; diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index 26a591e..2dd2a5e 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -1,157 +1,93 @@ -use crate::{Context, ReactionHandler, ReactionWatcher}; +use crate::{Context, Result}; +use futures_util::{future::Future, StreamExt}; use serenity::{ - builder::EditMessage, - framework::standard::{CommandError, CommandResult}, + collector::ReactionAction, model::{ - channel::{Message, Reaction, ReactionType}, + channel::{Message, ReactionType}, id::ChannelId, }, }; +use std::convert::TryFrom; +use tokio::time as tokio_time; const ARROW_RIGHT: &'static str = "➡️"; const ARROW_LEFT: &'static str = "⬅️"; -impl ReactionWatcher { - /// Start a pagination. - /// - /// Takes a copy of Context (which you can `clone`), a pager (see "Pagination") and a target channel id. - /// Pagination will handle all events on adding/removing an "arrow" emoji (⬅️ and ➡️). - /// This is a blocking call - it will block the thread until duration is over. - pub fn paginate( - &self, - ctx: Context, - channel: ChannelId, - pager: T, - duration: std::time::Duration, - ) -> CommandResult { - let handler = PaginationHandler::new(pager, ctx, channel)?; - self.handle_reactions(handler, duration, |_| {}); - Ok(()) - } - - /// A version of `paginate` that compiles for closures. - /// - /// A workaround until https://github.com/rust-lang/rust/issues/36582 is solved. - pub fn paginate_fn( - &self, - ctx: Context, - channel: ChannelId, - pager: T, - duration: std::time::Duration, - ) -> CommandResult - where - T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult) - + Send - + 'static, - { - self.paginate(ctx, channel, pager, duration) - } -} - -/// Pagination allows the bot to display content in multiple pages. -/// -/// You need to implement the "render_page" function, which takes a dummy content and -/// embed assigning function. -/// Pagination is automatically implemented for functions with the same signature as `render_page`. -/// -/// Pages start at 0. -pub trait Pagination { - /// Render a page. - /// - /// This would either create or edit a message, but you should not be worry about it. - fn render_page<'a>( - &mut self, - page: u8, - target: &'a mut EditMessage, - ) -> (&'a mut EditMessage, CommandResult); -} - -impl Pagination for T +/// Paginate! with a pager function. +/// If awaited, will block until everything is done. +pub async fn paginate<'a, T, F>( + mut pager: T, + ctx: &'a Context, + channel: ChannelId, + timeout: std::time::Duration, +) -> Result<()> where - T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult), + T: FnMut(u8, &Context, &Message) -> F, + F: Future>, { - fn render_page<'a>( - &mut self, - page: u8, - target: &'a mut EditMessage, - ) -> (&'a mut EditMessage, CommandResult) { - self(page, target) - } + let message = channel + .send_message(&ctx, |e| e.content("Youmu is loading the first page...")) + .await?; + // React to the message + message + .react(&ctx, ReactionType::try_from(ARROW_LEFT)?) + .await?; + message + .react(&ctx, ReactionType::try_from(ARROW_RIGHT)?) + .await?; + // Build a reaction collector + let mut reaction_collector = message.await_reactions(&ctx).await; + let mut page = 0; + + // Loop the handler function. + let res: Result<()> = loop { + match tokio_time::timeout(timeout, reaction_collector.next()).await { + Err(_) => break Ok(()), + Ok(None) => break Ok(()), + Ok(Some(reaction)) => { + page = match handle_reaction(page, &mut pager, ctx, &message, &reaction).await { + Ok(v) => v, + Err(e) => break Err(e), + }; + } + } + }; + + message.react(&ctx, '🛑').await?; + + res } -struct PaginationHandler { - pager: T, - message: Message, +// Handle the reaction and return a new page number. +async fn handle_reaction<'a, T, F>( page: u8, - ctx: Context, -} - -impl PaginationHandler { - pub fn new(pager: T, mut ctx: Context, channel: ChannelId) -> Result { - let message = channel.send_message(&mut ctx, |e| { - e.content("Youmu is loading the first page...") - })?; - // React to the message - message.react(&mut ctx, ARROW_LEFT)?; - message.react(&mut ctx, ARROW_RIGHT)?; - let mut p = Self { - pager, - message: message.clone(), - page: 0, - ctx, - }; - p.call_pager()?; - Ok(p) - } -} - -impl PaginationHandler { - /// Call the pager, log the error (if any). - fn call_pager(&mut self) -> CommandResult { - let mut res: CommandResult = Ok(()); - let mut msg = self.message.clone(); - msg.edit(self.ctx.http.clone(), |e| { - let (e, r) = self.pager.render_page(self.page, e); - res = r; - e - })?; - self.message = msg; - res - } -} - -impl Drop for PaginationHandler { - fn drop(&mut self) { - self.message.react(&self.ctx, "🛑").ok(); - } -} - -impl ReactionHandler for PaginationHandler { - fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult { - if reaction.message_id != self.message.id { - return Ok(()); - } - match &reaction.emoji { - ReactionType::Unicode(ref s) => match s.as_str() { - ARROW_LEFT if self.page == 0 => return Ok(()), - ARROW_LEFT => { - self.page -= 1; - if let Err(e) = self.call_pager() { - self.page += 1; - return Err(e); - } - } - ARROW_RIGHT => { - self.page += 1; - if let Err(e) = self.call_pager() { - self.page -= 1; - return Err(e); - } - } - _ => (), - }, - _ => (), - } - Ok(()) + pager: &mut T, + ctx: &'a Context, + message: &'_ Message, + reaction: &ReactionAction, +) -> Result +where + T: for<'n, 'm> FnMut(u8, &'n Context, &'m Message) -> F, + F: Future>, +{ + let reaction = match reaction { + ReactionAction::Added(v) | ReactionAction::Removed(v) => v, + }; + match &reaction.emoji { + ReactionType::Unicode(ref s) => match s.as_str() { + ARROW_LEFT if page == 0 => Ok(page), + ARROW_LEFT => Ok(if pager(page - 1, ctx, message).await? { + page - 1 + } else { + page + }), + ARROW_RIGHT => Ok(if pager(page + 1, ctx, message).await? { + page + 1 + } else { + page + }), + _ => Ok(page), + }, + _ => Ok(page), } } diff --git a/youmubot-prelude/src/reaction_watch.rs b/youmubot-prelude/src/reaction_watch.rs deleted file mode 100644 index 9cd5d6f..0000000 --- a/youmubot-prelude/src/reaction_watch.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crossbeam_channel::{after, bounded, select, Sender}; -use serenity::{framework::standard::CommandResult, model::channel::Reaction, prelude::*}; -use std::sync::{Arc, Mutex}; - -/// Handles a reaction. -/// -/// Every handler needs an expire time too. -pub trait ReactionHandler { - /// Handle a reaction. This is fired on EVERY reaction. - /// You do the filtering yourself. - /// - /// If `is_added` is false, the reaction was removed instead of added. - fn handle_reaction(&mut self, reaction: &Reaction, is_added: bool) -> CommandResult; -} - -impl ReactionHandler for T -where - T: FnMut(&Reaction, bool) -> CommandResult, -{ - fn handle_reaction(&mut self, reaction: &Reaction, is_added: bool) -> CommandResult { - self(reaction, is_added) - } -} - -/// The store for a set of dynamic reaction handlers. -#[derive(Debug, Clone)] -pub struct ReactionWatcher { - channels: Arc, bool)>>>>, -} - -impl TypeMapKey for ReactionWatcher { - type Value = ReactionWatcher; -} - -impl ReactionWatcher { - /// Create a new ReactionWatcher. - pub fn new() -> Self { - Self { - channels: Arc::new(Mutex::new(vec![])), - } - } - /// Send a reaction. - /// If `is_added` is false, the reaction was removed. - pub fn send(&self, r: Reaction, is_added: bool) { - let r = Arc::new(r); - self.channels - .lock() - .expect("Poisoned!") - .retain(|e| e.send((r.clone(), is_added)).is_ok()); - } - /// React! to a series of reaction - /// - /// The reactions stop after `duration` of idle. - pub fn handle_reactions( - &self, - mut h: H, - duration: std::time::Duration, - callback: impl FnOnce(H) -> () + Send + 'static, - ) { - let (send, reactions) = bounded(0); - { - self.channels.lock().expect("Poisoned!").push(send); - } - std::thread::spawn(move || { - loop { - let timeout = after(duration); - let r = select! { - recv(reactions) -> r => { let (r, is_added) = r.unwrap(); h.handle_reaction(&*r, is_added) }, - recv(timeout) -> _ => break, - }; - if let Err(v) = r { - dbg!(v); - } - } - callback(h) - }); - } - /// React! to a series of reaction - /// - /// The handler will stop after `duration` no matter what. - pub fn handle_reactions_timed( - &self, - mut h: H, - duration: std::time::Duration, - callback: impl FnOnce(H) -> () + Send + 'static, - ) { - let (send, reactions) = bounded(0); - { - self.channels.lock().expect("Poisoned!").push(send); - } - std::thread::spawn(move || { - let timeout = after(duration); - loop { - let r = select! { - recv(reactions) -> r => { let (r, is_added) = r.unwrap(); h.handle_reaction(&*r, is_added) }, - recv(timeout) -> _ => break, - }; - if let Err(v) = r { - dbg!(v); - } - } - callback(h); - }); - } -}