From 4b5dcfd0720133ff8dabc443ca6fd79ff0503f34 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 8 Feb 2020 18:37:17 -0500 Subject: [PATCH] Implement codeforces contest/problem hook --- Cargo.lock | 6 +- youmubot-cf/Cargo.toml | 3 + youmubot-cf/src/hook.rs | 145 ++++++++++++++++++++++++++++++++++++++++ youmubot-cf/src/lib.rs | 3 + youmubot/src/main.rs | 4 +- 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 youmubot-cf/src/hook.rs diff --git a/Cargo.lock b/Cargo.lock index 2d01632..9746240 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,10 +163,11 @@ dependencies = [ [[package]] name = "codeforces" version = "0.1.0" -source = "git+https://github.com/natsukagami/rust-codeforces-api#093c9fdb40d369a1390918d982d2bff55c984c81" +source = "git+https://github.com/natsukagami/rust-codeforces-api#3ec1dc2a97c8225a5ba6bafee517080fc9ae88f7" dependencies = [ "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1718,6 +1719,9 @@ version = "0.1.0" dependencies = [ "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", "codeforces 0.1.0 (git+https://github.com/natsukagami/rust-codeforces-api)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/youmubot-cf/Cargo.toml b/youmubot-cf/Cargo.toml index ff272eb..aafd64c 100644 --- a/youmubot-cf/Cargo.toml +++ b/youmubot-cf/Cargo.toml @@ -11,5 +11,8 @@ reqwest = "0.10.1" serenity = "0.8" Inflector = "0.11" codeforces = { git = "https://github.com/natsukagami/rust-codeforces-api" } +regex = "1" +lazy_static = "1" +rayon = "1" youmubot-prelude = { path = "../youmubot-prelude" } diff --git a/youmubot-cf/src/hook.rs b/youmubot-cf/src/hook.rs new file mode 100644 index 0000000..a3dcd97 --- /dev/null +++ b/youmubot-cf/src/hook.rs @@ -0,0 +1,145 @@ +use codeforces::{Contest, Problem}; +use lazy_static::lazy_static; +use rayon::{iter::Either, prelude::*}; +use regex::{Captures, Regex}; +use serenity::{ + builder::CreateEmbed, + framework::standard::{CommandError, CommandResult}, + model::channel::Message, + utils::MessageBuilder, +}; +use youmubot_prelude::*; + +lazy_static! { + static ref CONTEST_LINK: Regex = Regex::new( + r"https?://codeforces\.com/(contest|gym)/(?P\d+)(?:/problem/(?P\w+))?" + ) + .unwrap(); + static ref PROBLEMSET_LINK: Regex = Regex::new( + r"https?://codeforces\.com/problemset/problem/(?P\d+)/(?P\w+)" + ) + .unwrap(); +} + +enum ContestOrProblem { + Contest(Contest, Vec), + Problem(Problem), +} + +/// Prints info whenever a problem or contest (or more) is sent on a channel. +pub fn codeforces_info_hook(ctx: &mut Context, m: &Message) { + if m.author.bot { + return; + } + let http = ctx.data.get_cloned::(); + let matches = CONTEST_LINK + .captures_iter(&m.content) + .chain(PROBLEMSET_LINK.captures_iter(&m.content)) + // .collect::>() + // .into_par_iter() + .filter_map(|v| match parse_capture(http.clone(), v) { + Ok(v) => Some(v), + Err(e) => { + dbg!(e); + None + } + }) + .collect::>(); + if !matches.is_empty() { + m.channel_id + .send_message(&ctx, |c| { + c.content("Here are the info of the given Codeforces links!") + .embed(|e| print_info_message(&matches[..], e)) + }) + .ok(); + } +} + +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)), + }); + 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() { + m.push_line("**Problems**").push_line(""); + for (problem, link) in problems { + m.push(" - [") + .push_bold_safe(format!( + "[{}{}] {}", + problem.contest_id.unwrap_or(0), + problem.index, + problem.name + )) + .push(format!("]({})", link)); + if let Some(p) = problem.points { + m.push(format!(" | **{:.0}** points", p)); + } + if let Some(p) = problem.rating { + m.push(format!(" | rating **{:.0}**", p)); + } + if !problem.tags.is_empty() { + m.push(format!(" | tags: ||`{}`||", problem.tags.join(", "))); + } + m.push_line(""); + } + } + m.push_line(""); + + if !contests.is_empty() { + m.push_bold_line("Contests").push_line(""); + for (contest, problems, link) in contests { + let duration: Duration = format!("{}s", contest.duration_seconds).parse().unwrap(); + m.push(" - [") + .push_bold_safe(&contest.name) + .push(format!("]({})", link)) + .push(format!( + " | **{}** problems | duration **{}**", + problems.len(), + duration + )); + if let Some(p) = &contest.prepared_by { + m.push(format!( + " | prepared by [{}](https://codeforces.com/profile/{})", + p, p + )); + } + m.push_line(""); + } + } + e.description(m.build()) +} + +fn parse_capture<'a>( + http: ::Value, + cap: Captures<'a>, +) -> Result<(ContestOrProblem, &'a str), CommandError> { + let contest: u64 = cap + .name("contest") + .ok_or(CommandError::from("Contest not captured"))? + .as_str() + .parse()?; + let (contest, problems, _) = Contest::standings(&http, contest, |f| f.limit(1, 1))?; + match cap.name("problem") { + Some(p) => { + for problem in problems { + if &problem.index == p.as_str() { + return Ok(( + ContestOrProblem::Problem(problem), + cap.get(0).unwrap().as_str(), + )); + } + } + Err("No such problem in contest".into()) + } + None => Ok(( + ContestOrProblem::Contest(contest, problems), + cap.get(0).unwrap().as_str(), + )), + } +} diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index dcab2a9..53f9604 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -8,6 +8,9 @@ use serenity::{ use youmubot_prelude::*; mod embed; +mod hook; + +pub use hook::codeforces_info_hook; #[group] #[prefix = "cf"] diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index 01e34a8..8f372bc 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -25,7 +25,7 @@ impl EventHandler for Handler { } fn message(&self, mut ctx: Context, message: 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) { @@ -51,6 +51,8 @@ fn main() { // Set up hooks #[cfg(feature = "osu")] handler.hooks.push(youmubot_osu::discord::hook); + #[cfg(feature = "codeforces")] + handler.hooks.push(youmubot_cf::codeforces_info_hook); // Sets up a client let mut client = {