Overhaul reaction handler to a thread spawning model

This commit is contained in:
Natsu Kagami 2020-07-05 00:42:02 -04:00
parent 03c3281029
commit 9e83501259
Signed by: nki
GPG key ID: 73376E117CD20735
6 changed files with 164 additions and 125 deletions

View file

@ -145,7 +145,7 @@ pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult {
ctx.data.get_cloned::<ReactionWatcher>().paginate_fn( ctx.data.get_cloned::<ReactionWatcher>().paginate_fn(
ctx.clone(), ctx.clone(),
m.channel_id, m.channel_id,
|page, e| { move |page, e| {
let page = page as usize; let page = page as usize;
let start = ITEMS_PER_PAGE * page; let start = ITEMS_PER_PAGE * page;
let end = ranks.len().min(start + ITEMS_PER_PAGE); let end = ranks.len().min(start + ITEMS_PER_PAGE);
@ -236,12 +236,17 @@ pub fn contestranks(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRe
// Table me // Table me
let ranks = ranks let ranks = ranks
.iter() .into_iter()
.flat_map(|v| { .flat_map(|v| {
v.party v.party
.members .members
.iter() .iter()
.filter_map(|m| members.get(&m.handle).map(|mem| (mem, m.handle.clone(), v))) .filter_map(|m| {
members
.get(&m.handle)
.cloned()
.map(|mem| (mem, m.handle.clone(), v.clone()))
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -15,7 +15,7 @@ fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult {
let db = DB::open(&*ctx.data.read()); let db = DB::open(&*ctx.data.read());
let db = db.borrow()?; let db = db.borrow()?;
let roles = db.get(&guild_id).filter(|v| !v.is_empty()); let roles = db.get(&guild_id).filter(|v| !v.is_empty()).cloned();
match roles { match roles {
None => { None => {
m.reply(&ctx, "No roles available for assigning.")?; m.reply(&ctx, "No roles available for assigning.")?;
@ -23,8 +23,8 @@ fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult {
Some(v) => { Some(v) => {
let roles = guild_id.to_partial_guild(&ctx)?.roles; let roles = guild_id.to_partial_guild(&ctx)?.roles;
let roles: Vec<_> = v let roles: Vec<_> = v
.iter() .into_iter()
.filter_map(|(_, role)| roles.get(&role.id).map(|r| (r, &role.description))) .filter_map(|(_, role)| roles.get(&role.id).cloned().map(|r| (r, role.description)))
.collect(); .collect();
const ROLES_PER_PAGE: usize = 8; const ROLES_PER_PAGE: usize = 8;
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE; let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
@ -33,7 +33,7 @@ fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult {
watcher.paginate_fn( watcher.paginate_fn(
ctx.clone(), ctx.clone(),
m.channel_id, m.channel_id,
|page, e| { move |page, e| {
let page = page as usize; let page = page as usize;
let start = page * ROLES_PER_PAGE; let start = page * ROLES_PER_PAGE;
let end = roles.len().min(start + ROLES_PER_PAGE); let end = roles.len().min(start + ROLES_PER_PAGE);

View file

@ -7,7 +7,7 @@ use serenity::{
}, },
utils::MessageBuilder, utils::MessageBuilder,
}; };
use std::collections::HashMap as Map; use std::collections::{HashMap as Map, HashSet as Set};
use std::time::Duration; use std::time::Duration;
use youmubot_prelude::{Duration as ParseDuration, *}; use youmubot_prelude::{Duration as ParseDuration, *};
@ -29,7 +29,10 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
} }
let question = args.single::<String>()?; let question = args.single::<String>()?;
let choices = if args.is_empty() { let choices = if args.is_empty() {
vec![("😍", "Yes! 😍".to_owned()), ("🤢", "No! 🤢".to_owned())] vec![
("😍".to_owned(), "Yes! 😍".to_owned()),
("🤢".to_owned(), "No! 🤢".to_owned()),
]
} else { } else {
let choices: Vec<_> = args.iter().map(|v| v.unwrap()).collect(); let choices: Vec<_> = args.iter().map(|v| v.unwrap()).collect();
if choices.len() < 2 { if choices.len() < 2 {
@ -73,7 +76,7 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
// Ok... now we post up a nice voting panel. // Ok... now we post up a nice voting panel.
let channel = msg.channel_id; let channel = msg.channel_id;
let author = &msg.author; let author = msg.author.clone();
let panel = channel.send_message(&ctx, |c| { let panel = channel.send_message(&ctx, |c| {
c.content("@here").embed(|e| { c.content("@here").embed(|e| {
e.author(|au| { e.author(|au| {
@ -90,100 +93,122 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
// React on all the choices // React on all the choices
choices choices
.iter() .iter()
.try_for_each(|(v, _)| panel.react(&ctx, *v))?; .try_for_each(|(v, _)| panel.react(&ctx, v.clone()))?;
let reaction_to_choice: Map<_, _> = choices.iter().map(|r| (r.0, &r.1)).collect(); // A handler for votes.
let mut user_reactions: Map<UserId, Vec<&str>> = Map::new(); struct VoteHandler {
pub ctx: Context,
pub msg: Message,
pub user_reactions: Map<String, Set<UserId>>,
pub panel: Message,
}
impl VoteHandler {
fn new(ctx: Context, msg: Message, panel: Message, choices: &[(String, String)]) -> Self {
VoteHandler {
ctx,
msg,
user_reactions: choices
.iter()
.map(|(v, _)| (v.clone(), Set::new()))
.collect(),
panel,
}
}
}
impl ReactionHandler for VoteHandler {
fn handle_reaction(&mut self, reaction: &Reaction, is_add: bool) -> CommandResult {
if reaction.message_id != self.panel.id {
return Ok(());
}
if reaction.user(&self.ctx)?.bot {
return Ok(());
}
let users = if let ReactionType::Unicode(ref s) = reaction.emoji {
if let Some(users) = self.user_reactions.get_mut(s.as_str()) {
users
} else {
return Ok(());
}
} else {
return Ok(());
};
if is_add {
users.insert(reaction.user_id);
} else {
users.remove(&reaction.user_id);
}
Ok(())
}
}
ctx.data ctx.data
.get_cloned::<ReactionWatcher>() .get_cloned::<ReactionWatcher>()
.handle_reactions_timed( .handle_reactions_timed(
|reaction: &Reaction, is_add| { VoteHandler::new(ctx.clone(), msg.clone(), panel, &choices),
if reaction.message_id != panel.id {
return Ok(());
}
if reaction.user(&ctx)?.bot {
return Ok(());
}
let choice = if let ReactionType::Unicode(ref s) = reaction.emoji {
if let Some(choice) = reaction_to_choice.get(s.as_str()) {
choice
} else {
return Ok(());
}
} else {
return Ok(());
};
if is_add {
user_reactions
.entry(reaction.user_id)
.or_default()
.push(choice);
} else {
user_reactions.entry(reaction.user_id).and_modify(|v| {
v.retain(|f| &f != choice);
});
}
Ok(())
},
*duration, *duration,
)?; move |vh| {
let result: Vec<(&str, Vec<UserId>)> = { let (ctx, msg, user_reactions, panel) =
let mut res: Map<&str, Vec<UserId>> = Map::new(); (vh.ctx, vh.msg, vh.user_reactions, vh.panel);
for (u, r) in user_reactions { let result: Vec<(String, Vec<UserId>)> = user_reactions
for t in r { .into_iter()
res.entry(t).or_default().push(u); .filter(|(_, users)| !users.is_empty())
} .map(|(choice, users)| (choice, users.into_iter().collect()))
} .collect();
res.into_iter().collect()
};
if result.len() == 0 { if result.len() == 0 {
msg.reply( msg.reply(
&ctx, &ctx,
MessageBuilder::new() MessageBuilder::new()
.push("no one answer your question ") .push("no one answer your question ")
.push_bold_safe(&question) .push_bold_safe(&question)
.push(", sorry 😭") .push(", sorry 😭")
.build(), .build(),
)?; )
} else { .ok();
channel.send_message(&ctx, |c| { } else {
c.content({ channel
let mut content = MessageBuilder::new(); .send_message(&ctx, |c| {
content c.content({
.push("@here, ") let mut content = MessageBuilder::new();
.push(author.mention()) content
.push(" previously asked ") .push("@here, ")
.push_bold_safe(&question) .push(author.mention())
.push(", and here are the results!"); .push(" previously asked ")
result.iter().for_each(|(choice, votes)| { .push_bold_safe(&question)
content .push(", and here are the results!");
.push("\n - ") result.iter().for_each(|(choice, votes)| {
.push_bold(format!("{}", votes.len())) content
.push(" voted for ") .push("\n - ")
.push_bold_safe(choice) .push_bold(format!("{}", votes.len()))
.push(": ") .push(" voted for ")
.push( .push_bold_safe(choice)
votes .push(": ")
.iter() .push(
.map(|v| v.mention()) votes
.collect::<Vec<_>>() .iter()
.join(", "), .map(|v| v.mention())
); .collect::<Vec<_>>()
}); .join(", "),
content.build() );
}) });
})?; content.build()
} })
panel.delete(&ctx)?; })
.ok();
}
panel.delete(&ctx).ok();
},
);
Ok(()) Ok(())
// unimplemented!(); // unimplemented!();
} }
// Pick a set of random n reactions! // Pick a set of random n reactions!
fn pick_n_reactions(n: usize) -> Result<Vec<&'static str>, Error> { fn pick_n_reactions(n: usize) -> Result<Vec<String>, Error> {
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
if n > MAX_CHOICES { if n > MAX_CHOICES {
Err(Error::from("Too many options")) Err(Error::from("Too many options"))
@ -191,7 +216,7 @@ fn pick_n_reactions(n: usize) -> Result<Vec<&'static str>, Error> {
let mut rand = rand::thread_rng(); let mut rand = rand::thread_rng();
Ok(REACTIONS Ok(REACTIONS
.choose_multiple(&mut rand, n) .choose_multiple(&mut rand, n)
.map(|v| *v) .map(|v| (*v).to_owned())
.collect()) .collect())
} }
} }

View file

@ -228,7 +228,7 @@ impl FromStr for Nth {
} }
} }
fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> CommandResult { fn list_plays(plays: Vec<Score>, mode: Mode, ctx: Context, m: &Message) -> CommandResult {
let watcher = ctx.data.get_cloned::<ReactionWatcher>(); let watcher = ctx.data.get_cloned::<ReactionWatcher>();
let osu = ctx.data.get_cloned::<BeatmapMetaCache>(); let osu = ctx.data.get_cloned::<BeatmapMetaCache>();
let beatmap_cache = ctx.data.get_cloned::<BeatmapCache>(); let beatmap_cache = ctx.data.get_cloned::<BeatmapCache>();
@ -245,7 +245,7 @@ fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> Command
watcher.paginate_fn( watcher.paginate_fn(
ctx, ctx,
m.channel_id, m.channel_id,
|page, e| { move |page, e| {
let page = page as usize; let page = page as usize;
let start = page * ITEMS_PER_PAGE; let start = page * ITEMS_PER_PAGE;
let end = plays.len().min(start + ITEMS_PER_PAGE); let end = plays.len().min(start + ITEMS_PER_PAGE);
@ -417,7 +417,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
} }
Nth::All => { Nth::All => {
let plays = osu.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))?; let plays = osu.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))?;
list_plays(&plays, mode, ctx.clone(), msg)?; list_plays(plays, mode, ctx.clone(), msg)?;
} }
} }
Ok(()) Ok(())
@ -549,7 +549,7 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
} }
Nth::All => { Nth::All => {
let plays = osu.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))?; let plays = osu.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))?;
list_plays(&plays, mode, ctx.clone(), msg)?; list_plays(plays, mode, ctx.clone(), msg)?;
} }
} }
Ok(()) Ok(())

View file

@ -17,7 +17,7 @@ impl ReactionWatcher {
/// Takes a copy of Context (which you can `clone`), a pager (see "Pagination") and a target channel id. /// 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 ➡️). /// 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. /// This is a blocking call - it will block the thread until duration is over.
pub fn paginate<T: Pagination>( pub fn paginate<T: Pagination + Send + 'static>(
&self, &self,
ctx: Context, ctx: Context,
channel: ChannelId, channel: ChannelId,
@ -25,7 +25,8 @@ impl ReactionWatcher {
duration: std::time::Duration, duration: std::time::Duration,
) -> CommandResult { ) -> CommandResult {
let handler = PaginationHandler::new(pager, ctx, channel)?; let handler = PaginationHandler::new(pager, ctx, channel)?;
self.handle_reactions(handler, duration) self.handle_reactions(handler, duration, |_| {});
Ok(())
} }
/// A version of `paginate` that compiles for closures. /// A version of `paginate` that compiles for closures.
@ -39,7 +40,9 @@ impl ReactionWatcher {
duration: std::time::Duration, duration: std::time::Duration,
) -> CommandResult ) -> CommandResult
where where
T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult), T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult)
+ Send
+ 'static,
{ {
self.paginate(ctx, channel, pager, duration) self.paginate(ctx, channel, pager, duration)
} }

View file

@ -51,49 +51,55 @@ impl ReactionWatcher {
/// React! to a series of reaction /// React! to a series of reaction
/// ///
/// The reactions stop after `duration` of idle. /// The reactions stop after `duration` of idle.
pub fn handle_reactions( pub fn handle_reactions<H: ReactionHandler + Send + 'static>(
&self, &self,
mut h: impl ReactionHandler, mut h: H,
duration: std::time::Duration, duration: std::time::Duration,
) -> CommandResult { callback: impl FnOnce(H) -> () + Send + 'static,
) {
let (send, reactions) = bounded(0); let (send, reactions) = bounded(0);
{ {
self.channels.lock().expect("Poisoned!").push(send); self.channels.lock().expect("Poisoned!").push(send);
} }
loop { std::thread::spawn(move || {
let timeout = after(duration); loop {
let r = select! { let timeout = after(duration);
recv(reactions) -> r => { let (r, is_added) = r.unwrap(); h.handle_reaction(&*r, is_added) }, let r = select! {
recv(timeout) -> _ => break, 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); if let Err(v) = r {
dbg!(v);
}
} }
} callback(h)
Ok(()) });
} }
/// React! to a series of reaction /// React! to a series of reaction
/// ///
/// The handler will stop after `duration` no matter what. /// The handler will stop after `duration` no matter what.
pub fn handle_reactions_timed( pub fn handle_reactions_timed<H: ReactionHandler + Send + 'static>(
&self, &self,
mut h: impl ReactionHandler, mut h: H,
duration: std::time::Duration, duration: std::time::Duration,
) -> CommandResult { callback: impl FnOnce(H) -> () + Send + 'static,
) {
let (send, reactions) = bounded(0); let (send, reactions) = bounded(0);
{ {
self.channels.lock().expect("Poisoned!").push(send); self.channels.lock().expect("Poisoned!").push(send);
} }
let timeout = after(duration); std::thread::spawn(move || {
loop { let timeout = after(duration);
let r = select! { loop {
recv(reactions) -> r => { let (r, is_added) = r.unwrap(); h.handle_reaction(&*r, is_added) }, let r = select! {
recv(timeout) -> _ => break, 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); if let Err(v) = r {
dbg!(v);
}
} }
} callback(h);
Ok(()) });
} }
} }