From 9e835012599d7d3052dfb57771b4eb165a451f5e Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sun, 5 Jul 2020 00:42:02 -0400 Subject: [PATCH] Overhaul reaction handler to a thread spawning model --- youmubot-cf/src/lib.rs | 11 +- youmubot-core/src/community/roles.rs | 8 +- youmubot-core/src/community/votes.rs | 195 ++++++++++++++----------- youmubot-osu/src/discord/mod.rs | 8 +- youmubot-prelude/src/pagination.rs | 9 +- youmubot-prelude/src/reaction_watch.rs | 58 ++++---- 6 files changed, 164 insertions(+), 125 deletions(-) diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 915f87d..8fd487d 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -145,7 +145,7 @@ pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult { ctx.data.get_cloned::().paginate_fn( ctx.clone(), m.channel_id, - |page, e| { + move |page, e| { let page = page as usize; let start = ITEMS_PER_PAGE * 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 let ranks = ranks - .iter() + .into_iter() .flat_map(|v| { v.party .members .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::>() }) .collect::>(); diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs index ac4e11a..4d3cab0 100644 --- a/youmubot-core/src/community/roles.rs +++ b/youmubot-core/src/community/roles.rs @@ -15,7 +15,7 @@ fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult { let db = DB::open(&*ctx.data.read()); 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 { None => { m.reply(&ctx, "No roles available for assigning.")?; @@ -23,8 +23,8 @@ fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult { Some(v) => { let roles = guild_id.to_partial_guild(&ctx)?.roles; let roles: Vec<_> = v - .iter() - .filter_map(|(_, role)| roles.get(&role.id).map(|r| (r, &role.description))) + .into_iter() + .filter_map(|(_, role)| roles.get(&role.id).cloned().map(|r| (r, role.description))) .collect(); const ROLES_PER_PAGE: usize = 8; 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( ctx.clone(), m.channel_id, - |page, e| { + move |page, e| { let page = page as usize; let start = page * ROLES_PER_PAGE; let end = roles.len().min(start + ROLES_PER_PAGE); diff --git a/youmubot-core/src/community/votes.rs b/youmubot-core/src/community/votes.rs index 3704d30..61152b0 100644 --- a/youmubot-core/src/community/votes.rs +++ b/youmubot-core/src/community/votes.rs @@ -7,7 +7,7 @@ use serenity::{ }, utils::MessageBuilder, }; -use std::collections::HashMap as Map; +use std::collections::{HashMap as Map, HashSet as Set}; use std::time::Duration; 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::()?; 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 { let choices: Vec<_> = args.iter().map(|v| v.unwrap()).collect(); 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. let channel = msg.channel_id; - let author = &msg.author; + let author = msg.author.clone(); let panel = channel.send_message(&ctx, |c| { c.content("@here").embed(|e| { e.author(|au| { @@ -90,100 +93,122 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { // React on all the choices choices .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(); - let mut user_reactions: Map> = Map::new(); + // A handler for votes. + struct VoteHandler { + pub ctx: Context, + pub msg: Message, + pub user_reactions: Map>, + + 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 .get_cloned::() .handle_reactions_timed( - |reaction: &Reaction, is_add| { - 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(()) - }, + VoteHandler::new(ctx.clone(), msg.clone(), panel, &choices), *duration, - )?; - let result: Vec<(&str, Vec)> = { - let mut res: Map<&str, Vec> = Map::new(); - for (u, r) in user_reactions { - for t in r { - res.entry(t).or_default().push(u); - } - } - res.into_iter().collect() - }; + move |vh| { + let (ctx, msg, user_reactions, panel) = + (vh.ctx, vh.msg, vh.user_reactions, vh.panel); + let result: Vec<(String, Vec)> = user_reactions + .into_iter() + .filter(|(_, users)| !users.is_empty()) + .map(|(choice, users)| (choice, users.into_iter().collect())) + .collect(); - if result.len() == 0 { - msg.reply( - &ctx, - MessageBuilder::new() - .push("no one answer your question ") - .push_bold_safe(&question) - .push(", sorry 😭") - .build(), - )?; - } else { - channel.send_message(&ctx, |c| { - c.content({ - let mut content = MessageBuilder::new(); - content - .push("@here, ") - .push(author.mention()) - .push(" previously asked ") - .push_bold_safe(&question) - .push(", and here are the results!"); - result.iter().for_each(|(choice, votes)| { - content - .push("\n - ") - .push_bold(format!("{}", votes.len())) - .push(" voted for ") - .push_bold_safe(choice) - .push(": ") - .push( - votes - .iter() - .map(|v| v.mention()) - .collect::>() - .join(", "), - ); - }); - content.build() - }) - })?; - } - panel.delete(&ctx)?; + if result.len() == 0 { + msg.reply( + &ctx, + MessageBuilder::new() + .push("no one answer your question ") + .push_bold_safe(&question) + .push(", sorry 😭") + .build(), + ) + .ok(); + } else { + channel + .send_message(&ctx, |c| { + c.content({ + let mut content = MessageBuilder::new(); + content + .push("@here, ") + .push(author.mention()) + .push(" previously asked ") + .push_bold_safe(&question) + .push(", and here are the results!"); + result.iter().for_each(|(choice, votes)| { + content + .push("\n - ") + .push_bold(format!("{}", votes.len())) + .push(" voted for ") + .push_bold_safe(choice) + .push(": ") + .push( + votes + .iter() + .map(|v| v.mention()) + .collect::>() + .join(", "), + ); + }); + content.build() + }) + }) + .ok(); + } + panel.delete(&ctx).ok(); + }, + ); Ok(()) // unimplemented!(); } // Pick a set of random n reactions! -fn pick_n_reactions(n: usize) -> Result, Error> { +fn pick_n_reactions(n: usize) -> Result, Error> { use rand::seq::SliceRandom; if n > MAX_CHOICES { Err(Error::from("Too many options")) @@ -191,7 +216,7 @@ fn pick_n_reactions(n: usize) -> Result, Error> { let mut rand = rand::thread_rng(); Ok(REACTIONS .choose_multiple(&mut rand, n) - .map(|v| *v) + .map(|v| (*v).to_owned()) .collect()) } } diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 2b17019..de9d8a2 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -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, mode: Mode, ctx: Context, m: &Message) -> CommandResult { let watcher = ctx.data.get_cloned::(); let osu = ctx.data.get_cloned::(); let beatmap_cache = ctx.data.get_cloned::(); @@ -245,7 +245,7 @@ fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> Command watcher.paginate_fn( ctx, m.channel_id, - |page, e| { + move |page, e| { let page = page as usize; let start = page * 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 => { 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(()) @@ -549,7 +549,7 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { } Nth::All => { 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(()) diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index 3add228..26a591e 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -17,7 +17,7 @@ impl ReactionWatcher { /// 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( + pub fn paginate( &self, ctx: Context, channel: ChannelId, @@ -25,7 +25,8 @@ impl ReactionWatcher { duration: std::time::Duration, ) -> CommandResult { 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. @@ -39,7 +40,9 @@ impl ReactionWatcher { duration: std::time::Duration, ) -> CommandResult 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) } diff --git a/youmubot-prelude/src/reaction_watch.rs b/youmubot-prelude/src/reaction_watch.rs index d2d0c71..9cd5d6f 100644 --- a/youmubot-prelude/src/reaction_watch.rs +++ b/youmubot-prelude/src/reaction_watch.rs @@ -51,49 +51,55 @@ impl ReactionWatcher { /// React! to a series of reaction /// /// The reactions stop after `duration` of idle. - pub fn handle_reactions( + pub fn handle_reactions( &self, - mut h: impl ReactionHandler, + mut h: H, duration: std::time::Duration, - ) -> CommandResult { + callback: impl FnOnce(H) -> () + Send + 'static, + ) { let (send, reactions) = bounded(0); { self.channels.lock().expect("Poisoned!").push(send); } - 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); + 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); + } } - } - Ok(()) + callback(h) + }); } /// React! to a series of reaction /// /// The handler will stop after `duration` no matter what. - pub fn handle_reactions_timed( + pub fn handle_reactions_timed( &self, - mut h: impl ReactionHandler, + mut h: H, duration: std::time::Duration, - ) -> CommandResult { + callback: impl FnOnce(H) -> () + Send + 'static, + ) { let (send, reactions) = bounded(0); { self.channels.lock().expect("Poisoned!").push(send); } - 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); + 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); + } } - } - Ok(()) + callback(h); + }); } }