From 75f4e403df0386da6e4c30bbd4fc00666c8bf4f9 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 11 Feb 2020 19:32:48 -0500 Subject: [PATCH] Contest caching --- Cargo.lock | 2 +- youmubot-cf/src/hook.rs | 97 +++++++++++++++++++++++++++++++++++------ youmubot-cf/src/lib.rs | 1 + 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 583e2ef..d5584fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,7 +163,7 @@ dependencies = [ [[package]] name = "codeforces" version = "0.1.0" -source = "git+https://github.com/natsukagami/rust-codeforces-api#3ec1dc2a97c8225a5ba6bafee517080fc9ae88f7" +source = "git+https://github.com/natsukagami/rust-codeforces-api#e10f2155df238fe5edd2f0d33cb8d6a4ce252e69" 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)", diff --git a/youmubot-cf/src/hook.rs b/youmubot-cf/src/hook.rs index 8a0a50e..2a345d8 100644 --- a/youmubot-cf/src/hook.rs +++ b/youmubot-cf/src/hook.rs @@ -6,11 +6,12 @@ use serenity::{ builder::CreateEmbed, framework::standard::CommandError, model::channel::Message, utils::MessageBuilder, }; +use std::{collections::HashMap, sync::Arc}; use youmubot_prelude::*; lazy_static! { static ref CONTEST_LINK: Regex = Regex::new( - r"https?://codeforces\.com/(contest|gym)/(?P\d+)(?:/problem/(?P\w+))?" + r"https?://codeforces\.com/(contest|gym)s?/(?P\d+)(?:/problem/(?P\w+))?" ) .unwrap(); static ref PROBLEMSET_LINK: Regex = Regex::new( @@ -20,28 +21,92 @@ lazy_static! { } enum ContestOrProblem { - Contest(Contest, Vec), + Contest(Contest, Option>), Problem(Problem), } +/// Caches the contest list. +#[derive(Clone, Debug, Default)] +pub struct ContestCache(Arc>)>>>); + +impl TypeMapKey for ContestCache { + type Value = ContestCache; +} + +impl ContestCache { + fn get( + &self, + http: &::Value, + contest_id: u64, + ) -> Result<(Contest, Option>), CommandError> { + let rl = self.0.read(); + match rl.get(&contest_id) { + Some(r @ (_, Some(_))) => Ok(r.clone()), + Some((c, None)) => match Contest::standings(http, contest_id, |f| f.limit(1, 1)) { + Ok((c, p, _)) => Ok({ + drop(rl); + self.0 + .write() + .entry(contest_id) + .or_insert((c, Some(p))) + .clone() + }), + Err(_) => Ok((c.clone(), None)), + }, + None => { + drop(rl); + // Step 1: try to fetch it individually + match Contest::standings(http, contest_id, |f| f.limit(1, 1)) { + Ok((c, p, _)) => Ok(self + .0 + .write() + .entry(contest_id) + .or_insert((c, Some(p))) + .clone()), + Err(codeforces::Error::Codeforces(s)) if s.ends_with("has not started") => { + // Fetch the entire list + { + let mut m = self.0.write(); + let contests = Contest::list(http, contest_id > 100_000)?; + contests.into_iter().for_each(|c| { + m.entry(c.id).or_insert((c, None)); + }); + } + self.0 + .read() + .get(&contest_id) + .cloned() + .ok_or("No contest found".into()) + } + Err(e) => Err(e.into()), + } + // Step 2: try to fetch the entire list. + } + } + } +} + /// 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 contest_cache = 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 - } - }) + .filter_map( + |v| match parse_capture(http.clone(), contest_cache.clone(), v) { + Ok(v) => Some(v), + Err(e) => { + dbg!(e); + None + } + }, + ) .collect::>(); if !matches.is_empty() { m.channel_id @@ -97,8 +162,11 @@ fn print_info_message<'a>( .push_bold_safe(&contest.name) .push(format!("]({})", link)) .push(format!( - " | **{}** problems | duration **{}**", - problems.len(), + " | {} | duration **{}**", + problems + .as_ref() + .map(|v| format!("{} | **{}** problems", contest.phase, v.len())) + .unwrap_or(format!("{}", contest.phase)), duration )); if let Some(p) = &contest.prepared_by { @@ -115,17 +183,18 @@ fn print_info_message<'a>( fn parse_capture<'a>( http: ::Value, + contest_cache: ContestCache, cap: Captures<'a>, ) -> Result<(ContestOrProblem, &'a str), CommandError> { - let contest: u64 = cap + let contest_id: 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))?; + let (contest, problems) = contest_cache.get(&http, contest_id)?; match cap.name("problem") { Some(p) => { - for problem in problems { + for problem in problems.ok_or(CommandError::from("Contest hasn't started"))? { if &problem.index == p.as_str() { return Ok(( ContestOrProblem::Problem(problem), diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 7823653..9365357 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -24,6 +24,7 @@ pub use hook::codeforces_info_hook; pub fn setup(path: &std::path::Path, data: &mut ShareMap, announcers: &mut AnnouncerHandler) { CfSavedUsers::insert_into(data, path.join("cf_saved_users.yaml")) .expect("Must be able to set up DB"); + data.insert::(hook::ContestCache::default()); announcers.add("codeforces", announcer::updates); }