mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-20 01:08:55 +00:00
Remove ReactionWatcher (use serenity collector!) and simplify paginate
This commit is contained in:
parent
a578ce5924
commit
a8958a20f2
5 changed files with 85 additions and 250 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue