From b776a1d26ea089f755d7bb4bb819041f26893985 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Mon, 7 Sep 2020 19:01:21 -0400 Subject: [PATCH] osu: Fix commands --- youmubot-osu/src/discord/mod.rs | 546 +++++++++++++----------- youmubot-osu/src/discord/server_rank.rs | 98 +++-- youmubot-osu/src/lib.rs | 35 +- 3 files changed, 376 insertions(+), 303 deletions(-) diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 8d1c7ce..d64fc48 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -5,7 +5,6 @@ use crate::{ request::{BeatmapRequestKind, UserID}, Client as OsuHttpClient, }; -use rayon::prelude::*; use serenity::{ framework::standard::{ macros::{command, group}, @@ -14,7 +13,7 @@ use serenity::{ model::channel::Message, utils::MessageBuilder, }; -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; use youmubot_prelude::*; mod announcer; @@ -36,7 +35,7 @@ use server_rank::SERVER_RANK_COMMAND; pub(crate) struct OsuClient; impl TypeMapKey for OsuClient { - type Value = OsuHttpClient; + type Value = Arc; } /// Sets up the osu! command handling section. @@ -50,9 +49,9 @@ impl TypeMapKey for OsuClient { /// - Commands on the "osu" prefix /// - Hooks. Hooks are completely opt-in. /// -pub fn setup( +pub async fn setup( path: &std::path::Path, - data: &mut ShareMap, + data: &mut TypeMap, announcers: &mut AnnouncerHandler, ) -> CommandResult { // Databases @@ -60,11 +59,10 @@ pub fn setup( OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.yaml"))?; // API client - let http_client = data.get_cloned::(); - let osu_client = OsuHttpClient::new( - http_client.clone(), + let http_client = data.get::().unwrap().clone(); + let osu_client = Arc::new(OsuHttpClient::new( std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."), - ); + )); data.insert::(osu_client.clone()); data.insert::(oppai_cache::BeatmapCache::new(http_client)); data.insert::(beatmap_cache::BeatmapMetaCache::new( @@ -72,7 +70,7 @@ pub fn setup( )); // Announcer - announcers.add(announcer::ANNOUNCER_KEY, announcer::updates); + announcers.add(announcer::ANNOUNCER_KEY, announcer::Announcer); Ok(()) } @@ -88,8 +86,8 @@ struct Osu; #[description = "Receive information about an user in osu!std mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn std(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Std) +pub async fn std(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Std).await } #[command] @@ -97,8 +95,8 @@ pub fn std(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { #[description = "Receive information about an user in osu!taiko mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn taiko(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Taiko) +pub async fn taiko(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Taiko).await } #[command] @@ -106,8 +104,8 @@ pub fn taiko(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { #[description = "Receive information about an user in osu!catch mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn catch(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Catch) +pub async fn catch(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Catch).await } #[command] @@ -115,8 +113,8 @@ pub fn catch(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { #[description = "Receive information about an user in osu!mania mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn mania(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Mania) +pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Mania).await } pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode); @@ -137,17 +135,15 @@ impl AsRef for BeatmapWithMode { #[description = "Save the given username as your username."] #[usage = "[username or user_id]"] #[num_args(1)] -pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let osu = ctx.data.get_cloned::(); +pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + let osu = data.get::().unwrap(); let user = args.single::()?; - let user: Option = osu.user(UserID::Auto(user), |f| f)?; + let user: Option = osu.user(UserID::Auto(user), |f| f).await?; match user { Some(u) => { - let db = OsuSavedUsers::open(&*ctx.data.read()); - let mut db = db.borrow_mut()?; - - db.insert( + OsuSavedUsers::open(&*data).borrow_mut()?.insert( msg.author.id, OsuUser { id: u.id, @@ -161,10 +157,11 @@ pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { .push("user has been set to ") .push_mono_safe(u.username) .build(), - )?; + ) + .await?; } None => { - msg.reply(&ctx, "user not found...")?; + msg.reply(&ctx, "user not found...").await?; } } Ok(()) @@ -187,7 +184,7 @@ impl FromStr for ModeArg { fn to_user_id_query( s: Option, - data: &ShareMap, + data: &TypeMap, msg: &Message, ) -> Result { let id = match s { @@ -223,151 +220,159 @@ impl FromStr for Nth { } } -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::(); - +async fn list_plays<'a>( + plays: Vec, + mode: Mode, + ctx: &'a Context, + m: &'a Message, +) -> CommandResult { + let plays = Arc::new(plays); if plays.is_empty() { - m.reply(&ctx, "No plays found")?; + m.reply(&ctx, "No plays found").await?; return Ok(()); } - let mut beatmaps: Vec> = vec![None; plays.len()]; - const ITEMS_PER_PAGE: usize = 5; let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; - watcher.paginate_fn( - ctx, - m.channel_id, - move |page, e| { - let page = page as usize; - let start = page * ITEMS_PER_PAGE; - let end = plays.len().min(start + ITEMS_PER_PAGE); - if start >= end { - return (e, Err(Error::from("No more pages"))); - } + paginate( + move |page, ctx, msg| { + let plays = plays.clone(); + Box::pin(async move { + let data = ctx.data.read().await; + let osu = data.get::().unwrap(); + let beatmap_cache = data.get::().unwrap(); + let page = page as usize; + let start = page * ITEMS_PER_PAGE; + let end = plays.len().min(start + ITEMS_PER_PAGE); + if start >= end { + return Ok(false); + } - let plays = &plays[start..end]; - let beatmaps = { - let b = &mut beatmaps[start..end]; - b.par_iter_mut() - .enumerate() - .map(|(i, v)| { - v.get_or_insert_with(|| { - if let Some(b) = osu.get_beatmap(plays[i].beatmap_id, mode).ok() { - let stars = beatmap_cache - .get_beatmap(b.beatmap_id) - .ok() - .and_then(|b| { - mode.to_oppai_mode().and_then(|mode| { - b.get_info_with(Some(mode), plays[i].mods).ok() - }) - }) - .map(|info| info.stars as f64) - .unwrap_or(b.difficulty.stars); - format!( - "[{:.1}*] {} - {} [{}] ({})", - stars, - b.artist, - b.title, - b.difficulty_name, - b.short_link(Some(mode), Some(plays[i].mods)), - ) - } else { - "FETCH_FAILED".to_owned() - } - }) + let plays = &plays[start..end]; + let beatmaps = plays + .iter() + .map(|play| async move { + let beatmap = osu.get_beatmap(play.beatmap_id, mode).await?; + let stars = { + let b = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?; + mode.to_oppai_mode() + .and_then(|mode| b.get_info_with(Some(mode), play.mods).ok()) + .map(|info| info.stars as f64) + .unwrap_or(beatmap.difficulty.stars) + }; + let r: Result<_> = Ok(format!( + "[{:.1}*] {} - {} [{}] ({})", + stars, + beatmap.artist, + beatmap.title, + beatmap.difficulty_name, + beatmap.short_link(Some(mode), Some(play.mods)), + )); + r }) - .collect::>() - }; - let pp = plays - .iter() - .map(|p| { - p.pp.map(|pp| format!("{:.2}pp", pp)) - .or_else(|| { - beatmap_cache.get_beatmap(p.beatmap_id).ok().and_then(|b| { - mode.to_oppai_mode().and_then(|op| { - b.get_pp_from( - oppai_rs::Combo::NonFC { - max_combo: p.max_combo as u32, - misses: p.count_miss as u32, - }, - p.accuracy(mode) as f32, - Some(op), - p.mods, - ) - .ok() - .map(|pp| format!("{:.2}pp [?]", pp)) - }) - }) - }) - .unwrap_or("-".to_owned()) - }) - .collect::>(); - let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2); - /*mods width*/ - let mw = plays - .iter() - .map(|v| v.mods.to_string().len()) - .max() - .unwrap() - .max(4); - /*beatmap names*/ - let bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7); + .collect::>() + .map(|v| v.unwrap_or("FETCH_FAILED".to_owned())) + .collect::>(); + let pp = plays + .iter() + .map(|p| async move { + match p.pp.map(|pp| format!("{:.2}pp", pp)) { + Some(v) => Ok(v), + None => { + let b = beatmap_cache.get_beatmap(p.beatmap_id).await?; + let r: Result<_> = Ok(mode + .to_oppai_mode() + .and_then(|op| { + b.get_pp_from( + oppai_rs::Combo::NonFC { + max_combo: p.max_combo as u32, + misses: p.count_miss as u32, + }, + p.accuracy(mode) as f32, + Some(op), + p.mods, + ) + .ok() + .map(|pp| format!("{:.2}pp [?]", pp)) + }) + .unwrap_or("-".to_owned())); + r + } + } + }) + .collect::>() + .map(|v| v.unwrap_or("-".to_owned())) + .collect::>(); + let (beatmaps, pp) = future::join(beatmaps, pp).await; + let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2); + /*mods width*/ + let mw = plays + .iter() + .map(|v| v.mods.to_string().len()) + .max() + .unwrap() + .max(4); + /*beatmap names*/ + let bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7); - let mut m = MessageBuilder::new(); - // Table header - m.push_line(format!( - " # | {:pw$} | accuracy | rank | {:mw$} | {:bw$}", - "pp", - "mods", - "beatmap", - pw = pw, - mw = mw, - bw = bw - )); - m.push_line(format!( - "------{:-3} | {:>pw$} | {:>8} | {:^4} | {:mw$} | {:bw$}", - id + start + 1, - pp[id], - format!("{:.2}%", play.accuracy(mode)), - play.rank.to_string(), - play.mods.to_string(), - beatmap, + " # | {:pw$} | accuracy | rank | {:mw$} | {:bw$}", + "pp", + "mods", + "beatmap", pw = pw, mw = mw, bw = bw )); - } - // End - let table = m.build().replace("```", "\\`\\`\\`"); - let mut m = MessageBuilder::new(); - m.push_codeblock(table, None).push_line(format!( - "Page **{}/{}**", - page + 1, - total_pages - )); - if let None = mode.to_oppai_mode() { - m.push_line("Note: star difficulty doesn't reflect mods applied."); - } else { - m.push_line("[?] means pp was predicted by oppai-rs."); - } - (e.content(m.build()), Ok(())) + m.push_line(format!( + "------{:-3} | {:>pw$} | {:>8} | {:^4} | {:mw$} | {:bw$}", + id + start + 1, + pp[id], + format!("{:.2}%", play.accuracy(mode)), + play.rank.to_string(), + play.mods.to_string(), + beatmap, + pw = pw, + mw = mw, + bw = bw + )); + } + // End + let table = m.build().replace("```", "\\`\\`\\`"); + let mut m = MessageBuilder::new(); + m.push_codeblock(table, None).push_line(format!( + "Page **{}/{}**", + page + 1, + total_pages + )); + if let None = mode.to_oppai_mode() { + m.push_line("Note: star difficulty doesn't reflect mods applied."); + } else { + m.push_line("[?] means pp was predicted by oppai-rs."); + } + msg.edit(ctx, |f| f.content(m.to_string())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60), ) + .await?; + Ok(()) } #[command] @@ -375,44 +380,49 @@ fn list_plays(plays: Vec, mode: Mode, ctx: Context, m: &Message) -> Comma #[usage = "#[the nth recent play = --all] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"] #[example = "#1 / taiko / natsukagami"] #[max_args(3)] -pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let nth = args.single::().unwrap_or(Nth::All); let mode = args.single::().unwrap_or(ModeArg(Mode::Std)).0; - let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*data, msg)?; - let osu = ctx.data.get_cloned::(); - let meta_cache = ctx.data.get_cloned::(); - let oppai = ctx.data.get_cloned::(); + let osu = data.get::().unwrap(); + let meta_cache = data.get::().unwrap(); + let oppai = data.get::().unwrap(); let user = osu - .user(user, |f| f.mode(mode))? + .user(user, |f| f.mode(mode)) + .await? .ok_or(Error::from("User not found"))?; match nth { Nth::Nth(nth) => { let recent_play = osu - .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth))? + .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth)) + .await? .into_iter() .last() .ok_or(Error::from("No such play"))?; - let beatmap = meta_cache - .get_beatmap(recent_play.beatmap_id, mode) - .unwrap(); - let content = oppai.get_beatmap(beatmap.beatmap_id)?; + let beatmap = meta_cache.get_beatmap(recent_play.beatmap_id, mode).await?; + let content = oppai.get_beatmap(beatmap.beatmap_id).await?; let beatmap_mode = BeatmapWithMode(beatmap, mode); - msg.channel_id.send_message(&ctx, |m| { - m.content(format!( - "{}: here is the play that you requested", - msg.author - )) - .embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user, None, m)) - })?; + msg.channel_id + .send_message(&ctx, |m| { + m.content(format!( + "{}: here is the play that you requested", + msg.author + )) + .embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user, None, m)) + }) + .await?; // Save the beatmap... - cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap_mode)?; + cache::save_beatmap(&*data, msg.channel_id, &beatmap_mode)?; } Nth::All => { - let plays = osu.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))?; - list_plays(plays, mode, ctx.clone(), msg)?; + let plays = osu + .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) + .await?; + list_plays(plays, mode, ctx, msg).await?; } } Ok(()) @@ -422,28 +432,33 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult #[description = "Show information from the last queried beatmap."] #[usage = "[mods = no mod]"] #[max_args(1)] -pub fn last(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let b = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?; +pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + let b = cache::get_beatmap(&*data, msg.channel_id)?; match b { Some(BeatmapWithMode(b, m)) => { let mods = args.find::().unwrap_or(Mods::NOMOD); - let info = ctx - .data - .get_cloned::() - .get_beatmap(b.beatmap_id)? + let info = data + .get::() + .unwrap() + .get_beatmap(b.beatmap_id) + .await? .get_info_with(m.to_oppai_mode(), mods) .ok(); - msg.channel_id.send_message(&ctx, |f| { - f.content(format!( - "{}: here is the beatmap you requested!", - msg.author - )) - .embed(|c| beatmap_embed(&b, m, mods, info, c)) - })?; + msg.channel_id + .send_message(&ctx, |f| { + f.content(format!( + "{}: here is the beatmap you requested!", + msg.author + )) + .embed(|c| beatmap_embed(&b, m, mods, info, c)) + }) + .await?; } None => { - msg.reply(&ctx, "No beatmap was queried on this channel.")?; + msg.reply(&ctx, "No beatmap was queried on this channel.") + .await?; } } @@ -454,36 +469,43 @@ pub fn last(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { #[aliases("c", "chk")] #[description = "Check your own or someone else's best record on the last beatmap."] #[max_args(1)] -pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let bm = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?; +pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + let bm = cache::get_beatmap(&*data, msg.channel_id)?; match bm { None => { - msg.reply(&ctx, "No beatmap queried on this channel.")?; + msg.reply(&ctx, "No beatmap queried on this channel.") + .await?; } Some(bm) => { let b = &bm.0; let m = bm.1; - let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*data, msg)?; - let osu = ctx.data.get_cloned::(); - let oppai = ctx.data.get_cloned::(); + let osu = data.get::().unwrap(); + let oppai = data.get::().unwrap(); - let content = oppai.get_beatmap(b.beatmap_id)?; + let content = oppai.get_beatmap(b.beatmap_id).await?; let user = osu - .user(user, |f| f)? + .user(user, |f| f) + .await? .ok_or(Error::from("User not found"))?; - let scores = osu.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))?; + let scores = osu + .scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m)) + .await?; if scores.is_empty() { - msg.reply(&ctx, "No scores found")?; + msg.reply(&ctx, "No scores found").await?; } for score in scores.into_iter() { - msg.channel_id.send_message(&ctx, |c| { - c.embed(|m| score_embed(&score, &bm, &content, &user, None, m)) - })?; + msg.channel_id + .send_message(&ctx, |c| { + c.embed(|m| score_embed(&score, &bm, &content, &user, None, m)) + }) + .await?; } } } @@ -493,27 +515,31 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult #[command] #[description = "Get the n-th top record of an user."] -#[usage = "#[n-th = --all] / [mode (std, taiko, catch, mania) = std / [username or user_id = your saved user id]"] -#[example = "#2 / taiko / natsukagami"] +#[usage = "[mode (std, taiko, catch, mania)] = std / #[n-th = --all] / [username or user_id = your saved user id]"] +#[example = "taiko / #2 / natsukagami"] #[max_args(3)] -pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let nth = args.single::().unwrap_or(Nth::All); let mode = args .single::() .map(|ModeArg(t)| t) .unwrap_or(Mode::Std); - let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*data, msg)?; - let osu = ctx.data.get_cloned::(); - let oppai = ctx.data.get_cloned::(); + let osu = data.get::().unwrap(); + let oppai = data.get::().unwrap(); let user = osu - .user(user, |f| f.mode(mode))? + .user(user, |f| f.mode(mode)) + .await? .ok_or(Error::from("User not found"))?; match nth { Nth::Nth(nth) => { - let top_play = osu.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth))?; + let top_play = osu + .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth)) + .await?; let rank = top_play.len() as u8; @@ -524,66 +550,80 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { let beatmap = osu .beatmaps(BeatmapRequestKind::Beatmap(top_play.beatmap_id), |f| { f.mode(mode, true) - })? + }) + .await? .into_iter() .next() .unwrap(); - let content = oppai.get_beatmap(beatmap.beatmap_id)?; + let content = oppai.get_beatmap(beatmap.beatmap_id).await?; let beatmap = BeatmapWithMode(beatmap, mode); - msg.channel_id.send_message(&ctx, |m| { - m.content(format!( - "{}: here is the play that you requested", - msg.author - )) - .embed(|m| score_embed(&top_play, &beatmap, &content, &user, Some(rank), m)) - })?; + msg.channel_id + .send_message(&ctx, |m| { + m.content(format!( + "{}: here is the play that you requested", + msg.author + )) + .embed(|m| score_embed(&top_play, &beatmap, &content, &user, Some(rank), m)) + }) + .await?; // Save the beatmap... - cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap)?; + cache::save_beatmap(&*data, msg.channel_id, &beatmap)?; } Nth::All => { - let plays = osu.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))?; - list_plays(plays, mode, ctx.clone(), msg)?; + let plays = osu + .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100)) + .await?; + list_plays(plays, mode, ctx, msg).await?; } } Ok(()) } -fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult { - let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; - let osu = ctx.data.get_cloned::(); - let cache = ctx.data.get_cloned::(); - let user = osu.user(user, |f| f.mode(mode))?; - let oppai = ctx.data.get_cloned::(); +async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult { + let data = ctx.data.read().await; + let user = to_user_id_query(args.single::().ok(), &*data, msg)?; + let osu = data.get::().unwrap(); + let cache = data.get::().unwrap(); + let user = osu.user(user, |f| f.mode(mode)).await?; + let oppai = data.get::().unwrap(); match user { Some(u) => { - let best = osu - .user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode))? + let best = match osu + .user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode)) + .await? .into_iter() .next() - .map(|m| -> Result<_, Error> { - let beatmap = cache.get_beatmap(m.beatmap_id, mode)?; - let info = mode - .to_oppai_mode() - .map(|mode| -> Result<_, Error> { - Ok(oppai - .get_beatmap(m.beatmap_id)? - .get_info_with(Some(mode), m.mods)?) - }) - .transpose()?; - Ok((m, BeatmapWithMode(beatmap, mode), info)) + { + Some(m) => { + let beatmap = cache.get_beatmap(m.beatmap_id, mode).await?; + let info = match mode.to_oppai_mode() { + Some(mode) => Some( + oppai + .get_beatmap(m.beatmap_id) + .await? + .get_info_with(Some(mode), m.mods)?, + ), + None => None, + }; + Some((m, BeatmapWithMode(beatmap, mode), info)) + } + None => None, + }; + msg.channel_id + .send_message(&ctx, |m| { + m.content(format!( + "{}: here is the user that you requested", + msg.author + )) + .embed(|m| user_embed(u, best, m)) }) - .transpose()?; - msg.channel_id.send_message(&ctx, |m| { - m.content(format!( - "{}: here is the user that you requested", - msg.author - )) - .embed(|m| user_embed(u, best, m)) - }) + .await?; } - None => msg.reply(&ctx, "🔍 user not found!"), - }?; + None => { + msg.reply(&ctx, "🔍 user not found!").await?; + } + }; Ok(()) } diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 62f7d2a..7061ba2 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -1,8 +1,7 @@ use super::{db::OsuSavedUsers, ModeArg}; use crate::models::Mode; use serenity::{ - builder::EditMessage, - framework::standard::{macros::command, Args, CommandError as Error, CommandResult}, + framework::standard::{macros::command, Args, CommandResult}, model::channel::Message, utils::MessageBuilder, }; @@ -15,15 +14,15 @@ const ITEMS_PER_PAGE: usize = 10; #[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 { +pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let mode = args.single::().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| { + let users = OsuSavedUsers::open(&*data).borrow()?.clone(); + let users = users + .into_iter() + .map(|(user_id, osu_user)| async move { + guild.member(&ctx, user_id).await.ok().and_then(|member| { osu_user .pp .get(mode as usize) @@ -32,7 +31,10 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes .map(|pp| (pp, member.distinct(), osu_user.last_update.clone())) }) }) - .collect::>(); + .collect::>() + .filter_map(|v| future::ready(v)) + .collect::>() + .await; let last_update = users.iter().map(|(_, _, a)| a).min().cloned(); let mut users = users .into_iter() @@ -41,46 +43,54 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes 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...")?; + m.reply(&ctx, "No saved users in the current server...") + .await?; return Ok(()); } + + let users = std::sync::Arc::new(users); 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) * ITEMS_PER_PAGE; - let end = (start + ITEMS_PER_PAGE).min(users.len()); - if start >= end { - return (e, Err(Error("No more items".to_owned()))); - } - let total_len = users.len(); - let users = &users[start..end]; - let username_len = users.iter().map(|(_, u)| u.len()).max().unwrap_or(8).max(8); - let mut content = MessageBuilder::new(); - content - .push_line("```") - .push_line("Rank | pp | Username") - .push_line(format!("-----------------{:-= end { + return Ok(false); + } + let total_len = users.len(); + let users = &users[start..end]; + let username_len = users.iter().map(|(_, u)| u.len()).max().unwrap_or(8).max(8); + let mut content = MessageBuilder::new(); 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(())) + .push_line("```") + .push_line("Rank | pp | Username") + .push_line(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() + )); + m.edit(ctx, |f| f.content(content.to_string())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60), - )?; + ) + .await?; Ok(()) } diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index a1da980..edb0430 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -9,14 +9,30 @@ use models::*; use request::builders::*; use request::*; use reqwest::{Client as HTTPClient, RequestBuilder, Response}; -use std::convert::TryInto; +use std::{convert::TryInto, sync::Arc}; +use tower; use youmubot_prelude::*; +/// The number of requests per minute to the osu! server. +const REQUESTS_PER_MINUTE: u64 = 50; + +type BoxedResp = + std::pin::Pin> + Send>>; /// Client is the client that will perform calls to the osu! api server. -#[derive(Debug)] pub struct Client { + http_client: RwLock< + Box< + dyn tower::Service< + reqwest::Request, + Response = Response, + Error = reqwest::Error, + Future = BoxedResp, + > + Send + + Sync, + >, + >, + client: Arc, key: String, - client: HTTPClient, } fn vec_try_into>(v: Vec) -> Result, T::Error> { @@ -31,17 +47,24 @@ fn vec_try_into>(v: Vec) -> Result, T:: impl Client { /// Create a new client from the given API key. - pub fn new(http_client: HTTPClient, key: String) -> Client { + pub fn new(key: String) -> Client { + let http_client = Arc::new(HTTPClient::new()); + let _http = http_client.clone(); + let srv = tower::ServiceBuilder::new() + .rate_limit(REQUESTS_PER_MINUTE, std::time::Duration::from_secs(60)) + .service(tower::service_fn(move |req| -> BoxedResp { + Box::pin(_http.execute(req)) + })); Client { key, + http_client: RwLock::new(Box::new(srv)), client: http_client, } } async fn build_request(&self, r: RequestBuilder) -> Result { let v = r.query(&[("k", &*self.key)]).build()?; - // dbg!(v.url()); - Ok(self.client.execute(v).await?) + Ok(self.http_client.write().await.call(v).await?) } pub async fn beatmaps(