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.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::<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.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);

View file

@ -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::<String>()?;
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<UserId, Vec<&str>> = Map::new();
// A handler for votes.
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
.get_cloned::<ReactionWatcher>()
.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<UserId>)> = {
let mut res: Map<&str, Vec<UserId>> = 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<UserId>)> = 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::<Vec<_>>()
.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::<Vec<_>>()
.join(", "),
);
});
content.build()
})
})
.ok();
}
panel.delete(&ctx).ok();
},
);
Ok(())
// unimplemented!();
}
// 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;
if n > MAX_CHOICES {
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();
Ok(REACTIONS
.choose_multiple(&mut rand, n)
.map(|v| *v)
.map(|v| (*v).to_owned())
.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 osu = ctx.data.get_cloned::<BeatmapMetaCache>();
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(
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(())

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.
/// 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<T: Pagination>(
pub fn paginate<T: Pagination + Send + 'static>(
&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)
}

View file

@ -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<H: ReactionHandler + Send + 'static>(
&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<H: ReactionHandler + Send + 'static>(
&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);
});
}
}