From 2913d9268230f3de68353e57f8e4996c7082ffa6 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Fri, 7 Feb 2020 12:42:27 -0500 Subject: [PATCH 1/6] Implement Pagination --- youmubot-prelude/src/lib.rs | 2 + youmubot-prelude/src/pagination.rs | 119 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 youmubot-prelude/src/pagination.rs diff --git a/youmubot-prelude/src/lib.rs b/youmubot-prelude/src/lib.rs index c2c3d6b..77a31a1 100644 --- a/youmubot-prelude/src/lib.rs +++ b/youmubot-prelude/src/lib.rs @@ -3,11 +3,13 @@ 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. diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs new file mode 100644 index 0000000..4025e18 --- /dev/null +++ b/youmubot-prelude/src/pagination.rs @@ -0,0 +1,119 @@ +use crate::{Context, ReactionHandler, ReactionWatcher}; +use serenity::{ + builder::EditMessage, + framework::standard::{CommandError, CommandResult}, + model::{ + channel::{Message, Reaction, ReactionType}, + id::ChannelId, + }, +}; + +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( + &self, + ctx: Context, + channel: ChannelId, + pager: T, + duration: std::time::Duration, + ) -> CommandResult { + let handler = PaginationHandler::new(pager, ctx, channel)?; + self.handle_reactions(handler, 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 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 { + pager: T, + message: Message, + page: u8, + ctx: Context, +} + +impl PaginationHandler { + pub fn new(pager: T, mut ctx: Context, channel: ChannelId) -> Result { + let message = channel.send_message(&mut ctx, |e| { + e.content("Youmu is loading the first page...") + })?; + // React to the message + message.react(&mut ctx, "⬅️")?; + message.react(&mut ctx, "➡️️")?; + let mut p = Self { + pager, + message: message.clone(), + page: 0, + ctx, + }; + p.call_pager()?; + Ok(p) + } +} + +impl PaginationHandler { + /// 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 + })?; + res + } +} + +impl ReactionHandler for PaginationHandler { + fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult { + match &reaction.emoji { + ReactionType::Unicode(ref s) => match s.as_str() { + "⬅" if self.page == 0 => return Ok(()), + "⬅" => { + self.page -= 1; + self.call_pager()?; + } + "➡" => { + self.page += 1; + self.call_pager()?; + } + _ => (), + }, + _ => (), + } + Ok(()) + } +} From 22668fe654cabfd3775660105a68d9823ef85eca Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Fri, 7 Feb 2020 13:06:24 -0500 Subject: [PATCH 2/6] Save pp in local db --- youmubot-osu/src/discord/announcer.rs | 25 +++++++++++-------------- youmubot-osu/src/discord/db.rs | 12 ++++++------ youmubot-osu/src/discord/mod.rs | 1 + 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/youmubot-osu/src/discord/announcer.rs b/youmubot-osu/src/discord/announcer.rs index 8ab7e8c..c938304 100644 --- a/youmubot-osu/src/discord/announcer.rs +++ b/youmubot-osu/src/discord/announcer.rs @@ -28,25 +28,20 @@ impl Announcer for OsuAnnouncer { let osu = d.get_cloned::(); // 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,8 @@ impl Announcer for OsuAnnouncer { }); } osu_user.last_update = chrono::Utc::now(); + osu_user.pp = pp_values; + dbg!(&osu_user); } // Update users *OsuSavedUsers::open(&*d.read()).borrow_mut()? = data; diff --git a/youmubot-osu/src/discord/db.rs b/youmubot-osu/src/discord/db.rs index a24d23b..3952873 100644 --- a/youmubot-osu/src/discord/db.rs +++ b/youmubot-osu/src/discord/db.rs @@ -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>; @@ -19,4 +17,6 @@ pub type OsuLastBeatmap = DB>; pub struct OsuUser { pub id: u64, pub last_update: DateTime, + #[serde(default)] + pub pp: Vec>, } diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 9d29fd8..1fd19c6 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -145,6 +145,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( From 33c6aae64d1c1304719cbca8f5ea63695a817479 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 8 Feb 2020 13:54:56 -0500 Subject: [PATCH 3/6] Tune Pagination to be less buggy and more friendly --- youmubot-prelude/src/pagination.rs | 40 +++++++++++++++++++++----- youmubot-prelude/src/reaction_watch.rs | 2 +- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index 4025e18..be4fee5 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -8,6 +8,9 @@ use serenity::{ }, }; +const ARROW_RIGHT: &'static str = "➡️"; +const ARROW_LEFT: &'static str = "⬅️"; + impl ReactionWatcher { /// Start a pagination. /// @@ -24,6 +27,22 @@ impl ReactionWatcher { 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( + &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. @@ -70,8 +89,8 @@ impl PaginationHandler { e.content("Youmu is loading the first page...") })?; // React to the message - message.react(&mut ctx, "⬅️")?; - message.react(&mut ctx, "➡️️")?; + message.react(&mut ctx, ARROW_LEFT)?; + message.react(&mut ctx, ARROW_RIGHT)?; let mut p = Self { pager, message: message.clone(), @@ -93,6 +112,7 @@ impl PaginationHandler { res = r; e })?; + self.message = msg; res } } @@ -101,14 +121,20 @@ impl ReactionHandler for PaginationHandler { fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult { match &reaction.emoji { ReactionType::Unicode(ref s) => match s.as_str() { - "⬅" if self.page == 0 => return Ok(()), - "⬅" => { + ARROW_LEFT if self.page == 0 => return Ok(()), + ARROW_LEFT => { self.page -= 1; - self.call_pager()?; + if let Err(e) = self.call_pager() { + self.page += 1; + return Err(e); + } } - "➡" => { + ARROW_RIGHT => { self.page += 1; - self.call_pager()?; + if let Err(e) = self.call_pager() { + self.page -= 1; + return Err(e); + } } _ => (), }, diff --git a/youmubot-prelude/src/reaction_watch.rs b/youmubot-prelude/src/reaction_watch.rs index cb83269..44faba2 100644 --- a/youmubot-prelude/src/reaction_watch.rs +++ b/youmubot-prelude/src/reaction_watch.rs @@ -67,7 +67,7 @@ impl ReactionWatcher { recv(timeout) -> _ => break, }; if let Err(v) = r { - return Err(v); + dbg!(v); } } Ok(()) From 3b24c1ec090e2d0f0e0574af0e3de079c36d5b47 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 8 Feb 2020 13:55:05 -0500 Subject: [PATCH 4/6] Implement server ranks command --- youmubot-osu/src/discord/mod.rs | 4 +- youmubot-osu/src/discord/server_rank.rs | 71 +++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 youmubot-osu/src/discord/server_rank.rs diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 1fd19c6..70dadea 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -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] diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs new file mode 100644 index 0000000..3365b88 --- /dev/null +++ b/youmubot-osu/src/discord/server_rank.rs @@ -0,0 +1,71 @@ +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 std::collections::HashMap; +use youmubot_prelude::*; + +#[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::().map(|v| v.0).unwrap_or(Mode::Std); + let guild = m.guild_id.expect("Guild-only command"); + let mut 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())) + }) + }) + .collect::>(); + 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(()); + } + ctx.data.get_cloned::().paginate_fn( + ctx.clone(), + m.channel_id, + move |page: u8, e: &mut EditMessage| { + let start = (page as usize) * 5; + if start >= users.len() { + return (e, Err(Error("No more items".to_owned()))); + } + let users = users.iter().skip(start).take(5); + 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!("#{}", id + start), + pp + )) + .push_line_safe(member); + } + content.push("```"); + (e.content(content.build()), Ok(())) + }, + std::time::Duration::from_secs(60), + )?; + + Ok(()) +} From f9672de5964b3d51f9e4f1643e4abd51aacff84a Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 8 Feb 2020 14:18:37 -0500 Subject: [PATCH 5/6] Polish up the command's output --- youmubot-osu/src/discord/server_rank.rs | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 3365b88..b7aa618 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -6,9 +6,10 @@ use serenity::{ model::channel::Message, utils::MessageBuilder, }; -use std::collections::HashMap; use youmubot_prelude::*; +const ITEMS_PER_PAGE: usize = 10; + #[command("ranks")] #[description = "See the server's ranks"] #[usage = "[mode (Std, Taiko, Catch, Mania) = Std]"] @@ -17,7 +18,7 @@ use youmubot_prelude::*; pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { let mode = args.single::().map(|v| v.0).unwrap_or(Mode::Std); let guild = m.guild_id.expect("Guild-only command"); - let mut users = OsuSavedUsers::open(&*ctx.data.read()) + let users = OsuSavedUsers::open(&*ctx.data.read()) .borrow() .expect("DB initialized") .iter() @@ -28,25 +29,32 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes .get(mode as usize) .cloned() .and_then(|pp| pp) - .map(|pp| (pp, member.distinct())) + .map(|pp| (pp, member.distinct(), osu_user.last_update.clone())) }) }) .collect::>(); + let last_update = users.iter().map(|(_, _, a)| a).min().cloned(); + let mut users = users + .into_iter() + .map(|(a, b, _)| (a, b)) + .collect::>(); 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::().paginate_fn( ctx.clone(), m.channel_id, move |page: u8, e: &mut EditMessage| { - let start = (page as usize) * 5; + let start = (page as usize) * ITEMS_PER_PAGE; if start >= users.len() { return (e, Err(Error("No more items".to_owned()))); } - let users = users.iter().skip(start).take(5); + let total_len = users.len(); + let users = users.iter().skip(start).take(ITEMS_PER_PAGE); let mut content = MessageBuilder::new(); content .push_line("```") @@ -56,12 +64,17 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes content .push(format!( "{:>4} | {:>7.2} | ", - format!("#{}", id + start), + format!("#{}", 1 + id + start), pp )) .push_line_safe(member); } - content.push("```"); + 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), From 302add1889a1aba03ac7fd8c266453612fb59002 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 8 Feb 2020 14:31:29 -0500 Subject: [PATCH 6/6] Clean up debugging information --- youmubot-osu/src/discord/announcer.rs | 1 - youmubot/src/main.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/youmubot-osu/src/discord/announcer.rs b/youmubot-osu/src/discord/announcer.rs index c938304..f7e3353 100644 --- a/youmubot-osu/src/discord/announcer.rs +++ b/youmubot-osu/src/discord/announcer.rs @@ -70,7 +70,6 @@ impl Announcer for OsuAnnouncer { } osu_user.last_update = chrono::Utc::now(); osu_user.pp = pp_values; - dbg!(&osu_user); } // Update users *OsuSavedUsers::open(&*d.read()).borrow_mut()? = data; diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index 7cc6c30..5488850 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -25,8 +25,7 @@ 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) {