Remove ReactionWatcher (use serenity collector!) and simplify paginate

This commit is contained in:
Natsu Kagami 2020-09-03 17:55:19 -04:00
parent a578ce5924
commit a8958a20f2
Signed by: nki
GPG key ID: 73376E117CD20735
5 changed files with 85 additions and 250 deletions

1
Cargo.lock generated
View file

@ -1975,6 +1975,7 @@ dependencies = [
"rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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", "youmubot-db 0.1.0",
] ]

View file

@ -7,12 +7,17 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
serenity = "0.9.0-rc.0"
anyhow = "1.0" anyhow = "1.0"
async-trait = "0.1" async-trait = "0.1"
futures-util = "0.3" futures-util = "0.3"
tokio = { version = "0.2", features = ["time"] }
youmubot-db = { path = "../youmubot-db" } youmubot-db = { path = "../youmubot-db" }
crossbeam-channel = "0.4" crossbeam-channel = "0.4"
reqwest = "0.10" reqwest = "0.10"
rayon = "1" rayon = "1"
chrono = "0.4" chrono = "0.4"
[dependencies.serenity]
version = "0.9.0-rc.0"
default-features = true
features = ["collector"]

View file

@ -4,13 +4,11 @@ use std::sync::Arc;
pub mod announcer; pub mod announcer;
pub mod args; pub mod args;
pub mod pagination; pub mod pagination;
pub mod reaction_watch;
pub mod setup; pub mod setup;
pub use announcer::{Announcer, AnnouncerHandler}; pub use announcer::{Announcer, AnnouncerHandler};
pub use args::{Duration, UsernameArg}; pub use args::{Duration, UsernameArg};
pub use pagination::Pagination; pub use pagination::paginate;
pub use reaction_watch::{ReactionHandler, ReactionWatcher};
/// Re-export the anyhow errors /// Re-export the anyhow errors
pub use anyhow::{Error, Result}; pub use anyhow::{Error, Result};

View file

@ -1,157 +1,93 @@
use crate::{Context, ReactionHandler, ReactionWatcher}; use crate::{Context, Result};
use futures_util::{future::Future, StreamExt};
use serenity::{ use serenity::{
builder::EditMessage, collector::ReactionAction,
framework::standard::{CommandError, CommandResult},
model::{ model::{
channel::{Message, Reaction, ReactionType}, channel::{Message, ReactionType},
id::ChannelId, id::ChannelId,
}, },
}; };
use std::convert::TryFrom;
use tokio::time as tokio_time;
const ARROW_RIGHT: &'static str = "➡️"; const ARROW_RIGHT: &'static str = "➡️";
const ARROW_LEFT: &'static str = "⬅️"; const ARROW_LEFT: &'static str = "⬅️";
impl ReactionWatcher { /// Paginate! with a pager function.
/// Start a pagination. /// If awaited, will block until everything is done.
/// pub async fn paginate<'a, T, F>(
/// Takes a copy of Context (which you can `clone`), a pager (see "Pagination") and a target channel id. mut pager: T,
/// Pagination will handle all events on adding/removing an "arrow" emoji (⬅️ and ➡️). ctx: &'a Context,
/// This is a blocking call - it will block the thread until duration is over.
pub fn paginate<T: Pagination + Send + 'static>(
&self,
ctx: Context,
channel: ChannelId, channel: ChannelId,
pager: T, timeout: std::time::Duration,
duration: std::time::Duration, ) -> Result<()>
) -> 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<T>(
&self,
ctx: Context,
channel: ChannelId,
pager: T,
duration: std::time::Duration,
) -> CommandResult
where where
T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult) T: FnMut(u8, &Context, &Message) -> F,
+ Send F: Future<Output = Result<bool>>,
+ 'static,
{ {
self.paginate(ctx, channel, pager, duration) let message = channel
} .send_message(&ctx, |e| e.content("Youmu is loading the first page..."))
} .await?;
/// 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<T> Pagination for T
where
T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult),
{
fn render_page<'a>(
&mut self,
page: u8,
target: &'a mut EditMessage,
) -> (&'a mut EditMessage, CommandResult) {
self(page, target)
}
}
struct PaginationHandler<T: Pagination> {
pager: T,
message: Message,
page: u8,
ctx: Context,
}
impl<T: Pagination> PaginationHandler<T> {
pub fn new(pager: T, mut ctx: Context, channel: ChannelId) -> Result<Self, CommandError> {
let message = channel.send_message(&mut ctx, |e| {
e.content("Youmu is loading the first page...")
})?;
// React to the message // React to the message
message.react(&mut ctx, ARROW_LEFT)?; message
message.react(&mut ctx, ARROW_RIGHT)?; .react(&ctx, ReactionType::try_from(ARROW_LEFT)?)
let mut p = Self { .await?;
pager, message
message: message.clone(), .react(&ctx, ReactionType::try_from(ARROW_RIGHT)?)
page: 0, .await?;
ctx, // Build a reaction collector
}; let mut reaction_collector = message.await_reactions(&ctx).await;
p.call_pager()?; let mut page = 0;
Ok(p)
} // 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?;
impl<T: Pagination> PaginationHandler<T> {
/// 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 res
} }
}
impl<T: Pagination> Drop for PaginationHandler<T> { // Handle the reaction and return a new page number.
fn drop(&mut self) { async fn handle_reaction<'a, T, F>(
self.message.react(&self.ctx, "🛑").ok(); page: u8,
} pager: &mut T,
} ctx: &'a Context,
message: &'_ Message,
impl<T: Pagination> ReactionHandler for PaginationHandler<T> { reaction: &ReactionAction,
fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult { ) -> Result<u8>
if reaction.message_id != self.message.id { where
return Ok(()); T: for<'n, 'm> FnMut(u8, &'n Context, &'m Message) -> F,
} F: Future<Output = Result<bool>>,
{
let reaction = match reaction {
ReactionAction::Added(v) | ReactionAction::Removed(v) => v,
};
match &reaction.emoji { match &reaction.emoji {
ReactionType::Unicode(ref s) => match s.as_str() { ReactionType::Unicode(ref s) => match s.as_str() {
ARROW_LEFT if self.page == 0 => return Ok(()), ARROW_LEFT if page == 0 => Ok(page),
ARROW_LEFT => { ARROW_LEFT => Ok(if pager(page - 1, ctx, message).await? {
self.page -= 1; page - 1
if let Err(e) = self.call_pager() { } else {
self.page += 1; page
return Err(e); }),
} ARROW_RIGHT => Ok(if pager(page + 1, ctx, message).await? {
} page + 1
ARROW_RIGHT => { } else {
self.page += 1; page
if let Err(e) = self.call_pager() { }),
self.page -= 1; _ => Ok(page),
return Err(e);
}
}
_ => (),
}, },
_ => (), _ => Ok(page),
}
Ok(())
} }
} }

View file

@ -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<T> 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<Mutex<Vec<Sender<(Arc<Reaction>, 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<H: ReactionHandler + Send + 'static>(
&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<H: ReactionHandler + Send + 'static>(
&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);
});
}
}