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,24 +93,42 @@ 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>>,
ctx.data pub panel: Message,
.get_cloned::<ReactionWatcher>() }
.handle_reactions_timed(
|reaction: &Reaction, is_add| { impl VoteHandler {
if reaction.message_id != panel.id { 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(()); return Ok(());
} }
if reaction.user(&ctx)?.bot { if reaction.user(&self.ctx)?.bot {
return Ok(()); return Ok(());
} }
let choice = if let ReactionType::Unicode(ref s) = reaction.emoji { let users = if let ReactionType::Unicode(ref s) = reaction.emoji {
if let Some(choice) = reaction_to_choice.get(s.as_str()) { if let Some(users) = self.user_reactions.get_mut(s.as_str()) {
choice users
} else { } else {
return Ok(()); return Ok(());
} }
@ -115,28 +136,27 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
return Ok(()); return Ok(());
}; };
if is_add { if is_add {
user_reactions users.insert(reaction.user_id);
.entry(reaction.user_id)
.or_default()
.push(choice);
} else { } else {
user_reactions.entry(reaction.user_id).and_modify(|v| { users.remove(&reaction.user_id);
v.retain(|f| &f != choice);
});
} }
Ok(()) Ok(())
}, }
}
ctx.data
.get_cloned::<ReactionWatcher>()
.handle_reactions_timed(
VoteHandler::new(ctx.clone(), msg.clone(), panel, &choices),
*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(
@ -146,9 +166,11 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
.push_bold_safe(&question) .push_bold_safe(&question)
.push(", sorry 😭") .push(", sorry 😭")
.build(), .build(),
)?; )
.ok();
} else { } else {
channel.send_message(&ctx, |c| { channel
.send_message(&ctx, |c| {
c.content({ c.content({
let mut content = MessageBuilder::new(); let mut content = MessageBuilder::new();
content content
@ -174,16 +196,19 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
}); });
content.build() content.build()
}) })
})?; })
.ok();
} }
panel.delete(&ctx)?; 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,15 +51,17 @@ 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);
} }
std::thread::spawn(move || {
loop { loop {
let timeout = after(duration); let timeout = after(duration);
let r = select! { let r = select! {
@ -70,20 +72,23 @@ impl ReactionWatcher {
dbg!(v); dbg!(v);
} }
} }
Ok(()) callback(h)
});
} }
/// 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);
} }
std::thread::spawn(move || {
let timeout = after(duration); let timeout = after(duration);
loop { loop {
let r = select! { let r = select! {
@ -94,6 +99,7 @@ impl ReactionWatcher {
dbg!(v); dbg!(v);
} }
} }
Ok(()) callback(h);
});
} }
} }