diff --git a/youmubot-cf/src/announcer.rs b/youmubot-cf/src/announcer.rs index cea3290..631a75a 100644 --- a/youmubot-cf/src/announcer.rs +++ b/youmubot-cf/src/announcer.rs @@ -9,8 +9,6 @@ use serenity::{http::CacheHttp, model::id::UserId, CacheAndHttp}; use std::sync::Arc; use youmubot_prelude::*; -type Reqwest = ::Value; - /// Updates the rating and rating changes of the users. pub struct Announcer; diff --git a/youmubot-cf/src/hook.rs b/youmubot-cf/src/hook.rs index 26504c8..b69d30f 100644 --- a/youmubot-cf/src/hook.rs +++ b/youmubot-cf/src/hook.rs @@ -1,9 +1,7 @@ -use crate::CFClient; use chrono::{TimeZone, Utc}; use codeforces::{Client, Contest, Problem}; use dashmap::DashMap as HashMap; use lazy_static::lazy_static; -use rayon::{iter::Either, prelude::*}; use regex::{Captures, Regex}; use serenity::{ builder::CreateEmbed, framework::standard::CommandError, model::channel::Message, @@ -104,7 +102,6 @@ impl Hook for InfoHook { return Ok(()); } let data = ctx.data.read().await; - let http = data.get::().unwrap(); let contest_cache = data.get::().unwrap(); let matches = parse(&m.content[..], contest_cache) .collect::>() @@ -138,11 +135,24 @@ fn print_info_message<'a>( info: &[(ContestOrProblem, &str)], e: &'a mut CreateEmbed, ) -> &'a mut CreateEmbed { - let (mut problems, contests): (Vec<_>, Vec<_>) = - info.par_iter().partition_map(|(v, l)| match v { - ContestOrProblem::Problem(p) => Either::Left((p, l)), - ContestOrProblem::Contest(c, p) => Either::Right((c, p, l)), - }); + let (problems, contests): (Vec<_>, Vec<_>) = info.iter().partition(|(v, _)| match v { + ContestOrProblem::Problem(_) => true, + ContestOrProblem::Contest(_, _) => false, + }); + let mut problems = problems + .into_iter() + .map(|(v, l)| match v { + ContestOrProblem::Problem(p) => (p, l), + _ => unreachable!(), + }) + .collect::>(); + let contests = contests + .into_iter() + .map(|(v, l)| match v { + ContestOrProblem::Contest(c, p) => (c, p, l), + _ => unreachable!(), + }) + .collect::>(); problems.sort_by(|(a, _), (b, _)| a.rating.unwrap_or(1500).cmp(&b.rating.unwrap_or(1500))); let mut m = MessageBuilder::new(); if !problems.is_empty() { diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 7b441d7..da182df 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -2,7 +2,7 @@ use codeforces::Contest; use serenity::{ framework::standard::{ macros::{command, group}, - Args, CommandError as Error, CommandResult, + Args, CommandResult, }, model::channel::Message, utils::MessageBuilder, @@ -27,7 +27,7 @@ impl TypeMapKey for CFClient { use db::{CfSavedUsers, CfUser}; -pub use hook::codeforces_info_hook; +pub use hook::InfoHook; /// Sets up the CF databases. pub async fn setup(path: &std::path::Path, data: &mut TypeMap, announcers: &mut AnnouncerHandler) { @@ -37,7 +37,7 @@ pub async fn setup(path: &std::path::Path, data: &mut TypeMap, announcers: &mut let client = Arc::new(codeforces::Client::new(http.clone())); data.insert::(hook::ContestCache::new(client.clone()).await.unwrap()); data.insert::(client); - announcers.add("codeforces", announcer::updates); + announcers.add("codeforces", announcer::Announcer); } #[group] @@ -53,40 +53,46 @@ pub struct Codeforces; #[usage = "[handle or tag = yourself]"] #[example = "natsukagami"] #[max_args(1)] -pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn profile(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let handle = args .single::() .unwrap_or(UsernameArg::mention(m.author.id)); - let http = ctx.data.get_cloned::(); + let http = data.get::().unwrap(); let handle = match handle { UsernameArg::Raw(s) => s, UsernameArg::Tagged(u) => { - let db = CfSavedUsers::open(&*ctx.data.read()); - let db = db.borrow()?; - match db.get(&u) { - Some(v) => v.handle.clone(), + let db = CfSavedUsers::open(&*data); + let user = db.borrow()?.get(&u).map(|u| u.handle.clone()); + match user { + Some(v) => v, None => { - m.reply(&ctx, "no saved account found.")?; + m.reply(&ctx, "no saved account found.").await?; return Ok(()); } } } }; - let account = codeforces::User::info(&http, &[&handle[..]])? + let account = codeforces::User::info(&http, &[&handle[..]]) + .await? .into_iter() .next(); match account { - Some(v) => m.channel_id.send_message(&ctx, |send| { - send.content(format!( - "{}: Here is the user that you requested", - m.author.mention() - )) - .embed(|e| embed::user_embed(&v, e)) - }), - None => m.reply(&ctx, "User not found"), + Some(v) => { + m.channel_id + .send_message(&ctx, |send| { + send.content(format!( + "{}: Here is the user that you requested", + m.author.mention() + )) + .embed(|e| embed::user_embed(&v, e)) + }) + .await + } + None => m.reply(&ctx, "User not found").await, }?; Ok(()) @@ -96,28 +102,32 @@ pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult #[description = "Link your Codeforces account to the Discord account, to enjoy Youmu's tracking capabilities."] #[usage = "[handle]"] #[num_args(1)] -pub fn save(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn save(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let handle = args.single::()?; - let http = ctx.data.get_cloned::(); + let http = data.get::().unwrap(); - let account = codeforces::User::info(&http, &[&handle[..]])? + let account = codeforces::User::info(&http, &[&handle[..]]) + .await? .into_iter() .next(); match account { None => { - m.reply(&ctx, "cannot find an account with such handle")?; + m.reply(&ctx, "cannot find an account with such handle") + .await?; } Some(acc) => { // Collect rating changes data. - let rating_changes = acc.rating_changes(&http)?; - let db = CfSavedUsers::open(&*ctx.data.read()); - let mut db = db.borrow_mut()?; + let rating_changes = acc.rating_changes(&http).await?; + let mut db = CfSavedUsers::open(&*data); m.reply( &ctx, format!("account `{}` has been linked to your account.", &acc.handle), - )?; - db.insert(m.author.id, CfUser::save(acc, rating_changes)); + ) + .await?; + db.borrow_mut()? + .insert(m.author.id, CfUser::save(acc, rating_changes)); } } @@ -128,9 +138,10 @@ pub fn save(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { #[description = "See the leaderboard of all people in the server."] #[only_in(guilds)] #[num_args(0)] -pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult { +pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { + let data = ctx.data.read().await; let everyone = { - let db = CfSavedUsers::open(&*ctx.data.read()); + let db = CfSavedUsers::open(&*data); let db = db.borrow()?; db.iter() .map(|(k, v)| (k.clone(), v.clone())) @@ -139,84 +150,98 @@ pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult { let guild = m.guild_id.expect("Guild-only command"); let mut ranks = everyone .into_iter() - .filter_map(|(id, cf_user)| guild.member(&ctx, id).ok().map(|mem| (mem, cf_user))) - .collect::>(); + .map(|(id, cf_user)| { + guild + .member(&ctx, id) + .map(|mem| mem.map(|mem| (mem, cf_user))) + }) + .collect::>() + .filter_map(|v| future::ready(v.ok())) + .collect::>() + .await; ranks.sort_by(|(_, a), (_, b)| b.rating.unwrap_or(-1).cmp(&a.rating.unwrap_or(-1))); if ranks.is_empty() { - m.reply(&ctx, "No saved users in this server.")?; + m.reply(&ctx, "No saved users in this server.").await?; return Ok(()); } + let ranks = Arc::new(ranks); + const ITEMS_PER_PAGE: usize = 10; let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap(); - ctx.data.get_cloned::().paginate_fn( - ctx.clone(), - m.channel_id, - move |page, e| { - let page = page as usize; - let start = ITEMS_PER_PAGE * page; - let end = ranks.len().min(start + ITEMS_PER_PAGE); - if start >= end { - return (e, Err(Error::from("No more pages"))); - } - let ranks = &ranks[start..end]; + paginate( + move |page, ctx, msg| { + let ranks = ranks.clone(); + Box::pin(async move { + let page = page as usize; + let start = ITEMS_PER_PAGE * page; + let end = ranks.len().min(start + ITEMS_PER_PAGE); + if start >= end { + return Ok(false); + } + let ranks = &ranks[start..end]; - let handle_width = ranks.iter().map(|(_, cfu)| cfu.handle.len()).max().unwrap(); - let username_width = ranks - .iter() - .map(|(mem, _)| mem.distinct().len()) - .max() - .unwrap(); + let handle_width = ranks.iter().map(|(_, cfu)| cfu.handle.len()).max().unwrap(); + let username_width = ranks + .iter() + .map(|(mem, _)| mem.distinct().len()) + .max() + .unwrap(); - let mut m = MessageBuilder::new(); - m.push_line("```"); + let mut m = MessageBuilder::new(); + m.push_line("```"); - // Table header - m.push_line(format!( - "Rank | Rating | {:hw$} | {:uw$}", - "Handle", - "Username", - hw = handle_width, - uw = username_width - )); - m.push_line(format!( - "----------------{:->hw$}---{:->uw$}", - "", - "", - hw = handle_width, - uw = username_width - )); - - for (id, (mem, cfu)) in ranks.iter().enumerate() { - let id = id + start + 1; + // Table header m.push_line(format!( - "{:>4} | {:>6} | {:hw$} | {:uw$}", - format!("#{}", id), - cfu.rating - .map(|v| v.to_string()) - .unwrap_or("----".to_owned()), - cfu.handle, - mem.distinct(), + "Rank | Rating | {:hw$} | {:uw$}", + "Handle", + "Username", + hw = handle_width, + uw = username_width + )); + m.push_line(format!( + "----------------{:->hw$}---{:->uw$}", + "", + "", hw = handle_width, uw = username_width )); - } - m.push_line("```"); - m.push(format!( - "Page **{}/{}**. Last updated **{}**", - page + 1, - total_pages, - last_updated.to_rfc2822() - )); + for (id, (mem, cfu)) in ranks.iter().enumerate() { + let id = id + start + 1; + m.push_line(format!( + "{:>4} | {:>6} | {:hw$} | {:uw$}", + format!("#{}", id), + cfu.rating + .map(|v| v.to_string()) + .unwrap_or("----".to_owned()), + cfu.handle, + mem.distinct(), + hw = handle_width, + uw = username_width + )); + } - (e.content(m.build()), Ok(())) + m.push_line("```"); + m.push(format!( + "Page **{}/{}**. Last updated **{}**", + page + 1, + total_pages, + last_updated.to_rfc2822() + )); + + msg.edit(ctx, |f| f.content(m.build())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60), - )?; + ) + .await?; Ok(()) } @@ -226,23 +251,27 @@ pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult { #[usage = "[the contest id]"] #[num_args(1)] #[only_in(guilds)] -pub fn contestranks(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn contestranks(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let contest_id: u64 = args.single()?; let guild = m.guild_id.unwrap(); // Guild-only command - let members = CfSavedUsers::open(&*ctx.data.read()).borrow()?.clone(); + let members = CfSavedUsers::open(&*data).borrow()?.clone(); let members = members .into_iter() - .filter_map(|(user_id, cf_user)| { + .map(|(user_id, cf_user)| { guild .member(&ctx, user_id) - .ok() - .map(|v| (cf_user.handle, v)) + .map(|v| v.map(|v| (cf_user.handle, v))) }) - .collect::>(); - let http = ctx.data.get_cloned::(); - let (contest, problems, ranks) = Contest::standings(&http, contest_id, |f| { + .collect::>() + .filter_map(|v| future::ready(v.ok())) + .collect::>() + .await; + let http = data.get::().unwrap(); + let (contest, problems, ranks) = Contest::standings(http, contest_id, |f| { f.handles(members.iter().map(|(k, _)| k.clone()).collect()) - })?; + }) + .await?; // Table me let ranks = ranks @@ -262,100 +291,111 @@ pub fn contestranks(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRe .collect::>(); if ranks.is_empty() { - m.reply(&ctx, "No one in this server participated in the contest...")?; + m.reply(&ctx, "No one in this server participated in the contest...") + .await?; return Ok(()); } + let ranks = Arc::new(ranks); + const ITEMS_PER_PAGE: usize = 10; let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; - ctx.data.get_cloned::().paginate_fn( - ctx.clone(), - m.channel_id, - move |page, e| { - let page = page as usize; - let start = page * ITEMS_PER_PAGE; - let end = ranks.len().min(start + ITEMS_PER_PAGE); - if start >= end { - return (e, Err(Error::from("no more pages to show"))); - } - let ranks = &ranks[start..end]; - let hw = ranks - .iter() - .map(|(mem, handle, _)| format!("{} ({})", handle, mem.distinct()).len()) - .max() - .unwrap_or(0) - .max(6); - let hackw = ranks - .iter() - .map(|(_, _, row)| { - format!( - "{}/{}", - row.successful_hack_count, row.unsuccessful_hack_count - ) - .len() - }) - .max() - .unwrap_or(0) - .max(5); + paginate( + move |page, ctx, msg| { + let contest = contest.clone(); + let problems = problems.clone(); + let ranks = ranks.clone(); + Box::pin(async move { + let page = page as usize; + let start = page * ITEMS_PER_PAGE; + let end = ranks.len().min(start + ITEMS_PER_PAGE); + if start >= end { + return Ok(false); + } + let ranks = &ranks[start..end]; + let hw = ranks + .iter() + .map(|(mem, handle, _)| format!("{} ({})", handle, mem.distinct()).len()) + .max() + .unwrap_or(0) + .max(6); + let hackw = ranks + .iter() + .map(|(_, _, row)| { + format!( + "{}/{}", + row.successful_hack_count, row.unsuccessful_hack_count + ) + .len() + }) + .max() + .unwrap_or(0) + .max(5); - let mut table = MessageBuilder::new(); - let mut header = MessageBuilder::new(); - // Header - header.push(format!( - " Rank | {:hw$} | Total | {:hackw$}", - "Handle", - "Hacks", - hw = hw, - hackw = hackw - )); - for p in &problems { - header.push(format!(" | {:4}", p.index)); - } - let header = header.build(); - table - .push_line(&header) - .push_line(format!("{:-5} | {:5.0} | {: 0.0 { - table.push(format!("{:^4.0}", p.points)); - } else if let Some(_) = p.best_submission_time_seconds { - table.push(format!("{:^4}", "?")); - } else if p.rejected_attempt_count > 0 { - table.push(format!("{:^4}", format!("-{}", p.rejected_attempt_count))); - } else { - table.push(format!("{:^4}", "")); - } + for p in &problems { + header.push(format!(" | {:4}", p.index)); } - table.push_line(""); - } + let header = header.build(); + table + .push_line(&header) + .push_line(format!("{:-5} | {:5.0} | {: 0.0 { + table.push(format!("{:^4.0}", p.points)); + } else if let Some(_) = p.best_submission_time_seconds { + table.push(format!("{:^4}", "?")); + } else if p.rejected_attempt_count > 0 { + table.push(format!("{:^4}", format!("-{}", p.rejected_attempt_count))); + } else { + table.push(format!("{:^4}", "")); + } + } + table.push_line(""); + } + + let mut m = MessageBuilder::new(); + m.push_bold_safe(&contest.name) + .push(" ") + .push_line(contest.url()) + .push_codeblock(table.build(), None) + .push_line(format!("Page **{}/{}**", page + 1, total_pages)); + msg.edit(ctx, |e| e.content(m.build())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, Duration::from_secs(60), ) + .await?; + Ok(()) } #[command] @@ -364,10 +404,10 @@ pub fn contestranks(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRe #[num_args(1)] #[required_permissions(MANAGE_CHANNELS)] #[only_in(guilds)] -pub fn watch(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn watch(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { let contest_id: u64 = args.single()?; - live::watch_contest(ctx, m.guild_id.unwrap(), m.channel_id, contest_id)?; + live::watch_contest(ctx, m.guild_id.unwrap(), m.channel_id, contest_id).await?; Ok(()) }