mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-18 16:28:55 +00:00
Merge branch 'master' into codeforces
This commit is contained in:
commit
61651e0b05
12 changed files with 407 additions and 84 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -201,6 +201,14 @@ dependencies = [
|
|||
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.7.2"
|
||||
|
@ -1762,6 +1770,7 @@ dependencies = [
|
|||
name = "youmubot-prelude"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"youmubot-db 0.1.0",
|
||||
|
@ -1796,6 +1805,7 @@ dependencies = [
|
|||
"checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d"
|
||||
"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
|
||||
"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
|
||||
"checksum crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c"
|
||||
"checksum crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca"
|
||||
"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac"
|
||||
"checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db"
|
||||
|
|
|
@ -2,13 +2,12 @@ use serenity::framework::standard::CommandError as Error;
|
|||
use serenity::{
|
||||
framework::standard::{macros::command, Args, CommandResult},
|
||||
model::{
|
||||
channel::{Message, MessageReaction, ReactionType},
|
||||
channel::{Message,Reaction, ReactionType},
|
||||
id::UserId,
|
||||
},
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use std::collections::HashMap as Map;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use youmubot_prelude::{Duration as ParseDuration, *};
|
||||
|
||||
|
@ -93,10 +92,50 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
.iter()
|
||||
.try_for_each(|(v, _)| panel.react(&ctx, *v))?;
|
||||
|
||||
// Start sleeping
|
||||
thread::sleep(*duration);
|
||||
let reaction_to_choice: Map<_, _> = choices.iter().map(|r| (r.0, &r.1)).collect();
|
||||
let mut user_reactions: Map<UserId, Vec<&str>> = Map::new();
|
||||
|
||||
ctx.data.get_cloned::<ReactionWatcher>().handle_reactions(
|
||||
|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(())
|
||||
},
|
||||
*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()
|
||||
};
|
||||
|
||||
let result = collect_reactions(ctx, &panel, &choices)?;
|
||||
if result.len() == 0 {
|
||||
msg.reply(
|
||||
&ctx,
|
||||
|
@ -141,61 +180,6 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
// unimplemented!();
|
||||
}
|
||||
|
||||
// Collect reactions and store them as a map from choice to
|
||||
fn collect_reactions<'a>(
|
||||
ctx: &mut Context,
|
||||
msg: &Message,
|
||||
choices: &'a [(&'static str, String)],
|
||||
) -> Result<Vec<(&'a str, Vec<UserId>)>, Error> {
|
||||
// Get a brand new version of the Message
|
||||
let reactions = msg.channel_id.message(&ctx, msg.id)?.reactions;
|
||||
let reaction_to_choice: Map<_, _> = choices.into_iter().map(|r| (r.0, &r.1)).collect();
|
||||
let mut vec: Vec<(&str, Vec<UserId>)> = Vec::new();
|
||||
reactions
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
if let ReactionType::Unicode(ref v) = r.reaction_type {
|
||||
reaction_to_choice
|
||||
.get(&&v[..])
|
||||
.cloned()
|
||||
.filter(|_| r.count > 1)
|
||||
.map(|choice| (r.clone(), choice))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.try_for_each(|(r, choice)| -> Result<_, Error> {
|
||||
let users = collect_reaction_users(ctx, &msg, &r)?;
|
||||
vec.push((choice, users));
|
||||
Ok(())
|
||||
})?;
|
||||
vec.sort_by(|(_, b): &(_, Vec<_>), (_, d)| d.len().cmp(&b.len()));
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
fn collect_reaction_users(
|
||||
ctx: &mut Context,
|
||||
msg: &Message,
|
||||
reaction: &MessageReaction,
|
||||
) -> Result<Vec<UserId>, Error> {
|
||||
let mut res = Vec::with_capacity(reaction.count as usize);
|
||||
(0..reaction.count)
|
||||
.step_by(100)
|
||||
.try_for_each(|_| -> Result<_, Error> {
|
||||
let user_ids = msg
|
||||
.reaction_users(
|
||||
&ctx,
|
||||
reaction.reaction_type.clone(),
|
||||
Some(100),
|
||||
res.last().cloned(),
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|i| i.id);
|
||||
res.extend(user_ids);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
// Pick a set of random n reactions!
|
||||
fn pick_n_reactions(n: usize) -> Result<Vec<&'static str>, Error> {
|
||||
use rand::seq::SliceRandom;
|
||||
|
|
|
@ -28,25 +28,20 @@ impl Announcer for OsuAnnouncer {
|
|||
let osu = d.get_cloned::<OsuClient>();
|
||||
// For each user...
|
||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||
for (user_id, osu_user) in data.iter_mut() {
|
||||
let mut user = None;
|
||||
'user_loop: for (user_id, osu_user) in data.iter_mut() {
|
||||
let mut pp_values = vec![]; // Store the pp values here...
|
||||
for mode in &[Mode::Std, Mode::Taiko, Mode::Mania, Mode::Catch] {
|
||||
let scores = OsuAnnouncer::scan_user(&osu, osu_user, *mode)?;
|
||||
if scores.is_empty() {
|
||||
if scores.is_empty() && !osu_user.pp.is_empty() {
|
||||
// Nothing to update: no new scores and pp is there.
|
||||
pp_values.push(osu_user.pp[*mode as usize]);
|
||||
continue;
|
||||
}
|
||||
let user = {
|
||||
let user = &mut user;
|
||||
if let None = user {
|
||||
match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
||||
Ok(u) => {
|
||||
*user = u;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
};
|
||||
user.as_ref().unwrap()
|
||||
let user = match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
||||
Ok(Some(u)) => u,
|
||||
_ => continue 'user_loop,
|
||||
};
|
||||
pp_values.push(user.pp);
|
||||
scores
|
||||
.into_par_iter()
|
||||
.filter_map(|(rank, score)| {
|
||||
|
@ -74,6 +69,7 @@ impl Announcer for OsuAnnouncer {
|
|||
});
|
||||
}
|
||||
osu_user.last_update = chrono::Utc::now();
|
||||
osu_user.pp = pp_values;
|
||||
}
|
||||
// Update users
|
||||
*OsuSavedUsers::open(&*d.read()).borrow_mut()? = data;
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
model::id::{ChannelId, UserId},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use youmubot_db::{DB};
|
||||
use crate::models::{Beatmap, Mode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::model::id::{ChannelId, UserId};
|
||||
use std::collections::HashMap;
|
||||
use youmubot_db::DB;
|
||||
|
||||
/// Save the user IDs.
|
||||
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
||||
|
@ -19,4 +17,6 @@ pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
|||
pub struct OsuUser {
|
||||
pub id: u64,
|
||||
pub last_update: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub pp: Vec<Option<f64>>,
|
||||
}
|
||||
|
|
|
@ -19,12 +19,14 @@ mod cache;
|
|||
mod db;
|
||||
pub(crate) mod embeds;
|
||||
mod hook;
|
||||
mod server_rank;
|
||||
|
||||
pub use announcer::OsuAnnouncer;
|
||||
use db::OsuUser;
|
||||
use db::{OsuLastBeatmap, OsuSavedUsers};
|
||||
use embeds::{beatmap_embed, score_embed, user_embed};
|
||||
pub use hook::hook;
|
||||
use server_rank::SERVER_RANK_COMMAND;
|
||||
|
||||
/// The osu! client.
|
||||
pub(crate) struct OsuClient;
|
||||
|
@ -68,7 +70,7 @@ pub fn setup(
|
|||
#[group]
|
||||
#[prefix = "osu"]
|
||||
#[description = "osu! related commands."]
|
||||
#[commands(std, taiko, catch, mania, save, recent, last, check, top)]
|
||||
#[commands(std, taiko, catch, mania, save, recent, last, check, top, server_rank)]
|
||||
struct Osu;
|
||||
|
||||
#[command]
|
||||
|
@ -145,6 +147,7 @@ pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
OsuUser {
|
||||
id: u.id,
|
||||
last_update: chrono::Utc::now(),
|
||||
pp: vec![],
|
||||
},
|
||||
);
|
||||
msg.reply(
|
||||
|
|
84
youmubot-osu/src/discord/server_rank.rs
Normal file
84
youmubot-osu/src/discord/server_rank.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use super::{db::OsuSavedUsers, ModeArg};
|
||||
use crate::models::Mode;
|
||||
use serenity::{
|
||||
builder::EditMessage,
|
||||
framework::standard::{macros::command, Args, CommandError as Error, CommandResult},
|
||||
model::channel::Message,
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
const ITEMS_PER_PAGE: usize = 10;
|
||||
|
||||
#[command("ranks")]
|
||||
#[description = "See the server's ranks"]
|
||||
#[usage = "[mode (Std, Taiko, Catch, Mania) = Std]"]
|
||||
#[max_args(1)]
|
||||
#[only_in(guilds)]
|
||||
pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult {
|
||||
let mode = args.single::<ModeArg>().map(|v| v.0).unwrap_or(Mode::Std);
|
||||
let guild = m.guild_id.expect("Guild-only command");
|
||||
let users = OsuSavedUsers::open(&*ctx.data.read())
|
||||
.borrow()
|
||||
.expect("DB initialized")
|
||||
.iter()
|
||||
.filter_map(|(user_id, osu_user)| {
|
||||
guild.member(&ctx, user_id).ok().and_then(|member| {
|
||||
osu_user
|
||||
.pp
|
||||
.get(mode as usize)
|
||||
.cloned()
|
||||
.and_then(|pp| pp)
|
||||
.map(|pp| (pp, member.distinct(), osu_user.last_update.clone()))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let last_update = users.iter().map(|(_, _, a)| a).min().cloned();
|
||||
let mut users = users
|
||||
.into_iter()
|
||||
.map(|(a, b, _)| (a, b))
|
||||
.collect::<Vec<_>>();
|
||||
users.sort_by(|(a, _), (b, _)| (*b).partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
if users.is_empty() {
|
||||
m.reply(&ctx, "No saved users in the current server...")?;
|
||||
return Ok(());
|
||||
}
|
||||
let last_update = last_update.unwrap();
|
||||
ctx.data.get_cloned::<ReactionWatcher>().paginate_fn(
|
||||
ctx.clone(),
|
||||
m.channel_id,
|
||||
move |page: u8, e: &mut EditMessage| {
|
||||
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||
if start >= users.len() {
|
||||
return (e, Err(Error("No more items".to_owned())));
|
||||
}
|
||||
let total_len = users.len();
|
||||
let users = users.iter().skip(start).take(ITEMS_PER_PAGE);
|
||||
let mut content = MessageBuilder::new();
|
||||
content
|
||||
.push_line("```")
|
||||
.push_line("Rank | pp | Username")
|
||||
.push_line("-------------------------");
|
||||
for (id, (pp, member)) in users.enumerate() {
|
||||
content
|
||||
.push(format!(
|
||||
"{:>4} | {:>7.2} | ",
|
||||
format!("#{}", 1 + id + start),
|
||||
pp
|
||||
))
|
||||
.push_line_safe(member);
|
||||
}
|
||||
content.push_line("```").push_line(format!(
|
||||
"Page **{}**/**{}**. Last updated: `{}`",
|
||||
page + 1,
|
||||
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
|
||||
last_update.to_rfc2822()
|
||||
));
|
||||
(e.content(content.build()), Ok(()))
|
||||
},
|
||||
std::time::Duration::from_secs(60),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -9,4 +9,5 @@ edition = "2018"
|
|||
[dependencies]
|
||||
serenity = "0.8"
|
||||
youmubot-db = { path = "../youmubot-db" }
|
||||
crossbeam-channel = "0.4"
|
||||
reqwest = "0.10"
|
||||
|
|
|
@ -3,10 +3,14 @@ use std::sync::Arc;
|
|||
|
||||
pub mod announcer;
|
||||
pub mod args;
|
||||
pub mod pagination;
|
||||
pub mod reaction_watch;
|
||||
pub mod setup;
|
||||
|
||||
pub use announcer::Announcer;
|
||||
pub use args::Duration;
|
||||
pub use pagination::Pagination;
|
||||
pub use reaction_watch::{ReactionHandler, ReactionWatcher};
|
||||
|
||||
/// The global app data.
|
||||
pub type AppData = Arc<RwLock<ShareMap>>;
|
||||
|
|
148
youmubot-prelude/src/pagination.rs
Normal file
148
youmubot-prelude/src/pagination.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use crate::{Context, ReactionHandler, ReactionWatcher};
|
||||
use serenity::{
|
||||
builder::EditMessage,
|
||||
framework::standard::{CommandError, CommandResult},
|
||||
model::{
|
||||
channel::{Message, Reaction, ReactionType},
|
||||
id::ChannelId,
|
||||
},
|
||||
};
|
||||
|
||||
const ARROW_RIGHT: &'static str = "➡️";
|
||||
const ARROW_LEFT: &'static str = "⬅️";
|
||||
|
||||
impl ReactionWatcher {
|
||||
/// Start a pagination.
|
||||
///
|
||||
/// 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>(
|
||||
&self,
|
||||
ctx: Context,
|
||||
channel: ChannelId,
|
||||
pager: T,
|
||||
duration: std::time::Duration,
|
||||
) -> CommandResult {
|
||||
let handler = PaginationHandler::new(pager, ctx, channel)?;
|
||||
self.handle_reactions(handler, duration)
|
||||
}
|
||||
|
||||
/// 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
|
||||
T: for<'a> Fn(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult),
|
||||
{
|
||||
self.paginate(ctx, channel, pager, duration)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>(
|
||||
&self,
|
||||
page: u8,
|
||||
target: &'a mut EditMessage,
|
||||
) -> (&'a mut EditMessage, CommandResult);
|
||||
}
|
||||
|
||||
impl<T> Pagination for T
|
||||
where
|
||||
T: for<'a> Fn(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult),
|
||||
{
|
||||
fn render_page<'a>(
|
||||
&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
|
||||
message.react(&mut ctx, ARROW_LEFT)?;
|
||||
message.react(&mut ctx, ARROW_RIGHT)?;
|
||||
let mut p = Self {
|
||||
pager,
|
||||
message: message.clone(),
|
||||
page: 0,
|
||||
ctx,
|
||||
};
|
||||
p.call_pager()?;
|
||||
Ok(p)
|
||||
}
|
||||
}
|
||||
|
||||
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, |e| {
|
||||
let (e, r) = self.pager.render_page(self.page, e);
|
||||
res = r;
|
||||
e
|
||||
})?;
|
||||
self.message = msg;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Pagination> ReactionHandler for PaginationHandler<T> {
|
||||
fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult {
|
||||
if reaction.message_id != self.message.id {
|
||||
return Ok(());
|
||||
}
|
||||
match &reaction.emoji {
|
||||
ReactionType::Unicode(ref s) => match s.as_str() {
|
||||
ARROW_LEFT if self.page == 0 => return Ok(()),
|
||||
ARROW_LEFT => {
|
||||
self.page -= 1;
|
||||
if let Err(e) = self.call_pager() {
|
||||
self.page += 1;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
ARROW_RIGHT => {
|
||||
self.page += 1;
|
||||
if let Err(e) = self.call_pager() {
|
||||
self.page -= 1;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
75
youmubot-prelude/src/reaction_watch.rs
Normal file
75
youmubot-prelude/src/reaction_watch.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
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`.
|
||||
pub fn handle_reactions(
|
||||
&self,
|
||||
mut h: impl ReactionHandler,
|
||||
duration: std::time::Duration,
|
||||
) -> CommandResult {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -9,5 +9,9 @@ pub fn setup_prelude(db_path: &Path, data: &mut ShareMap, _: &mut StandardFramew
|
|||
crate::announcer::AnnouncerChannels::insert_into(data, db_path.join("announcers.yaml"))
|
||||
.expect("Announcers DB set up");
|
||||
|
||||
// Set up the HTTP client.
|
||||
data.insert::<crate::HTTPClient>(reqwest::blocking::Client::new());
|
||||
|
||||
// Set up the reaction watcher.
|
||||
data.insert::<crate::ReactionWatcher>(crate::ReactionWatcher::new());
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@ use dotenv;
|
|||
use dotenv::var;
|
||||
use serenity::{
|
||||
framework::standard::{DispatchError, StandardFramework},
|
||||
model::{channel::Message, gateway},
|
||||
model::{
|
||||
channel::{Message, Reaction},
|
||||
gateway,
|
||||
},
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
|
@ -22,8 +25,19 @@ impl EventHandler for Handler {
|
|||
}
|
||||
|
||||
fn message(&self, mut ctx: Context, message: Message) {
|
||||
println!("{:?}", message);
|
||||
self.hooks.iter().for_each(|f| f(&mut ctx, &message));
|
||||
self.hooks.iter().for_each(|f| f(&mut ctx, &message));
|
||||
}
|
||||
|
||||
fn reaction_add(&self, ctx: Context, reaction: Reaction) {
|
||||
ctx.data
|
||||
.get_cloned::<ReactionWatcher>()
|
||||
.send(reaction, true);
|
||||
}
|
||||
|
||||
fn reaction_remove(&self, ctx: Context, reaction: Reaction) {
|
||||
ctx.data
|
||||
.get_cloned::<ReactionWatcher>()
|
||||
.send(reaction, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue