From 4284c6c9082521188dbc1671ee9bcf6d7979efe6 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Fri, 31 Jan 2020 23:21:24 -0500 Subject: [PATCH 01/10] Codeforces library minimally written Implement a lot of new structs Embed for User Implement more data structures impl Embed for Rating Change Move codeforces package outside --- Cargo.lock | 20 +++++++++ Cargo.toml | 1 + youmubot-cf/Cargo.toml | 12 +++++ youmubot-cf/src/embed.rs | 96 ++++++++++++++++++++++++++++++++++++++++ youmubot-cf/src/lib.rs | 1 + 5 files changed, 130 insertions(+) create mode 100644 youmubot-cf/Cargo.toml create mode 100644 youmubot-cf/src/embed.rs create mode 100644 youmubot-cf/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3210d13..5941f03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,15 @@ dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "codeforces" +version = "0.1.0" +source = "git+https://github.com/natsukagami/rust-codeforces-api#2eb51a5ee5eb10b725ffbb33a4250a052eb6c60f" +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)", +] + [[package]] name = "command_attr" version = "0.1.7" @@ -1685,6 +1694,16 @@ dependencies = [ "youmubot-prelude 0.1.0", ] +[[package]] +name = "youmubot-cf" +version = "0.1.0" +dependencies = [ + "codeforces 0.1.0 (git+https://github.com/natsukagami/rust-codeforces-api)", + "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)", +] + [[package]] name = "youmubot-core" version = "0.1.0" @@ -1759,6 +1778,7 @@ dependencies = [ "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum codeforces 0.1.0 (git+https://github.com/natsukagami/rust-codeforces-api)" = "" "checksum command_attr 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b61098146d3e0ad56c4918ae30ab9f32a7222cc859fc65fbc2a8475c1e48b336" "checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" "checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" diff --git a/Cargo.toml b/Cargo.toml index 828c358..7fd1f61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "youmubot-prelude", "youmubot-db", "youmubot-core", + "youmubot-cf", "youmubot-osu", "youmubot", ] diff --git a/youmubot-cf/Cargo.toml b/youmubot-cf/Cargo.toml new file mode 100644 index 0000000..1a5ebde --- /dev/null +++ b/youmubot-cf/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "youmubot-cf" +version = "0.1.0" +authors = ["Natsu Kagami "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +serde = { version = "1", features = ["derive"] } +reqwest = "0.10.1" +serenity = "0.8" +codeforces = { git = "https://github.com/natsukagami/rust-codeforces-api" } diff --git a/youmubot-cf/src/embed.rs b/youmubot-cf/src/embed.rs new file mode 100644 index 0000000..1084f6e --- /dev/null +++ b/youmubot-cf/src/embed.rs @@ -0,0 +1,96 @@ +use codeforces::{Contest, RatingChange, User}; +use serenity::{builder::CreateEmbed, utils::MessageBuilder}; +use std::borrow::Borrow; + +fn unwrap_or_ref<'a, T: ?Sized, B: Borrow>(opt: &'a Option, default: &'a T) -> &'a T { + opt.as_ref().map(|v| v.borrow()).unwrap_or(default) +} + +/// Create an embed representing the user. +pub fn user_embed<'a>(user: &User, e: &'a mut CreateEmbed) -> &'a mut CreateEmbed { + let rank = unwrap_or_ref(&user.rank, "Unranked"); + let max_rank = unwrap_or_ref(&user.max_rank, "Unranked"); + let rating = user.rating.unwrap_or(1500); + let max_rating = user.max_rating.unwrap_or(1500); + let name = &[&user.first_name, &user.last_name] + .iter() + .filter_map(|v| v.as_ref().map(|v| v.as_str())) + .collect::>() + .join(" "); + let place = &[&user.organization, &user.city, &user.country] + .iter() + .filter_map(|v| v.as_ref().map(|v| v.as_str())) + .collect::>() + .join(" "); + e.color(user.color()) + .author(|a| a.name(rank)) + .title(&user.handle) + .description(format!( + "{}\n{}", + if name == "" { + "".to_owned() + } else { + format!("**{}**", name) + }, + if place == "" { + "".to_owned() + } else { + format!("from **{}**", place) + } + )) + .field( + "Rating", + format!("**{}** (max **{}**)", rating, max_rating), + true, + ) + .field("Contribution", format!("**{}**", user.contribution), true) + .field( + "Rank", + format!("**{}** (max **{}**)", rank, max_rank), + false, + ) +} + +/// Gets an embed of the Rating Change. +pub fn rating_change_embed<'a>( + rating_change: &RatingChange, + user: &User, + contest: &Contest, + tag: &str, + e: &'a mut CreateEmbed, +) -> &'a mut CreateEmbed { + let delta = (rating_change.new_rating as i64) - (rating_change.old_rating as i64); + let color = if delta < 0 { 0xff0000 } else { 0x00ff00 }; + let message = if delta < 0 { + MessageBuilder::new() + .push(tag) + .push(" competed in ") + .push_bold_safe(&contest.name) + .push(", gaining ") + .push_bold_safe(delta) + .push(" rating placing at ") + .push_bold(format!("#{}", rating_change.rank)) + .push("! 🎂🎂🎂") + .build() + } else { + MessageBuilder::new() + .push(tag) + .push(" competed in ") + .push_bold_safe(&contest.name) + .push(", but lost ") + .push_bold_safe(-delta) + .push(" rating placing at ") + .push_bold(format!("#{}", rating_change.rank)) + .push("... 😭😭😭") + .build() + }; + + e.author(|a| { + a.icon_url(&user.avatar) + .url(user.profile_url()) + .name(&user.handle) + }) + .color(color) + .description(message) + .field("Contest Link", contest.url(), true) +} diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs new file mode 100644 index 0000000..057a3a2 --- /dev/null +++ b/youmubot-cf/src/lib.rs @@ -0,0 +1 @@ +pub mod embed; From 7c77b7fd61c032c17d4d79805057ba0fa862f06b Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Thu, 6 Feb 2020 14:15:06 -0500 Subject: [PATCH 02/10] Load youmubot-cf --- youmubot-cf/src/lib.rs | 46 +++++++++++++++++++++++++++++++++++++++++- youmubot/Cargo.toml | 4 +++- youmubot/src/main.rs | 4 ++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 057a3a2..dcab2a9 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -1 +1,45 @@ -pub mod embed; +use serenity::{ + framework::standard::{ + macros::{command, group}, + Args, CommandResult, + }, + model::{channel::Message, id::UserId}, +}; +use youmubot_prelude::*; + +mod embed; + +#[group] +#[prefix = "cf"] +#[description = "Codeforces-related commands"] +#[commands(profile)] +#[default_command(profile)] +pub struct Codeforces; + +#[command] +#[aliases("p", "show", "u", "user", "get")] +#[description = "Get an user's profile"] +#[usage = "[handle or tag = yourself]"] +#[example = "natsukagami"] +#[num_args(1)] +pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { + let handle = args.single::()?; + let http = ctx.data.get_cloned::(); + + let account = codeforces::User::info(&http, &[&handle[..]])? + .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"), + }?; + + Ok(()) +} diff --git a/youmubot/Cargo.toml b/youmubot/Cargo.toml index 5d5469a..c20d200 100644 --- a/youmubot/Cargo.toml +++ b/youmubot/Cargo.toml @@ -6,9 +6,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["core", "osu"] +default = ["core", "osu", "codeforces"] core = [] osu = ["youmubot-osu"] +codeforces = ["youmubot-cf"] [dependencies] serenity = "0.8" @@ -17,4 +18,5 @@ youmubot-db = { path = "../youmubot-db" } youmubot-prelude = { path = "../youmubot-prelude" } youmubot-core = { path = "../youmubot-core" } youmubot-osu = { path = "../youmubot-osu", optional = true } +youmubot-cf = { path = "../youmubot-cf", optional = true } diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index 54e765b..a7e0526 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -71,6 +71,8 @@ fn main() { println!("Core enabled."); #[cfg(feature = "osu")] println!("osu! enabled."); + #[cfg(feature = "codeforces")] + println!("codeforces enabled."); client.with_framework(fw); @@ -155,5 +157,7 @@ fn setup_framework(client: &Client) -> StandardFramework { .group(&youmubot_core::COMMUNITY_GROUP); #[cfg(feature = "osu")] let fw = fw.group(&youmubot_osu::discord::OSU_GROUP); + #[cfg(feature = "codeforces")] + let fw = fw.group(&youmubot_cf::CODEFORCES_GROUP); fw } From 28ed92eb2feac93661d4cea8d8f61ed4124c8896 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Thu, 6 Feb 2020 14:15:34 -0500 Subject: [PATCH 03/10] Minor changes on user embed --- Cargo.lock | 15 ++++++++++++++- youmubot-cf/Cargo.toml | 3 +++ youmubot-cf/src/embed.rs | 13 ++++++++----- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5941f03..7cb5cad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "adler32" version = "1.0.4" @@ -154,7 +163,7 @@ dependencies = [ [[package]] name = "codeforces" version = "0.1.0" -source = "git+https://github.com/natsukagami/rust-codeforces-api#2eb51a5ee5eb10b725ffbb33a4250a052eb6c60f" +source = "git+https://github.com/natsukagami/rust-codeforces-api#093c9fdb40d369a1390918d982d2bff55c984c81" 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)", @@ -1688,6 +1697,7 @@ version = "0.1.0" dependencies = [ "dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "youmubot-cf 0.1.0", "youmubot-core 0.1.0", "youmubot-db 0.1.0", "youmubot-osu 0.1.0", @@ -1698,10 +1708,12 @@ dependencies = [ name = "youmubot-cf" 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)", "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)", + "youmubot-prelude 0.1.0", ] [[package]] @@ -1756,6 +1768,7 @@ dependencies = [ ] [metadata] +"checksum Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" "checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" "checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" "checksum anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)" = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" diff --git a/youmubot-cf/Cargo.toml b/youmubot-cf/Cargo.toml index 1a5ebde..ff272eb 100644 --- a/youmubot-cf/Cargo.toml +++ b/youmubot-cf/Cargo.toml @@ -9,4 +9,7 @@ edition = "2018" serde = { version = "1", features = ["derive"] } reqwest = "0.10.1" serenity = "0.8" +Inflector = "0.11" codeforces = { git = "https://github.com/natsukagami/rust-codeforces-api" } + +youmubot-prelude = { path = "../youmubot-prelude" } diff --git a/youmubot-cf/src/embed.rs b/youmubot-cf/src/embed.rs index 1084f6e..be81961 100644 --- a/youmubot-cf/src/embed.rs +++ b/youmubot-cf/src/embed.rs @@ -1,4 +1,5 @@ use codeforces::{Contest, RatingChange, User}; +use inflector::Inflector; use serenity::{builder::CreateEmbed, utils::MessageBuilder}; use std::borrow::Borrow; @@ -8,8 +9,8 @@ fn unwrap_or_ref<'a, T: ?Sized, B: Borrow>(opt: &'a Option, default: &'a T /// Create an embed representing the user. pub fn user_embed<'a>(user: &User, e: &'a mut CreateEmbed) -> &'a mut CreateEmbed { - let rank = unwrap_or_ref(&user.rank, "Unranked"); - let max_rank = unwrap_or_ref(&user.max_rank, "Unranked"); + let rank = unwrap_or_ref(&user.rank, "Unranked").to_title_case(); + let max_rank = unwrap_or_ref(&user.max_rank, "Unranked").to_title_case(); let rating = user.rating.unwrap_or(1500); let max_rating = user.max_rating.unwrap_or(1500); let name = &[&user.first_name, &user.last_name] @@ -21,10 +22,12 @@ pub fn user_embed<'a>(user: &User, e: &'a mut CreateEmbed) -> &'a mut CreateEmbe .iter() .filter_map(|v| v.as_ref().map(|v| v.as_str())) .collect::>() - .join(" "); + .join(", "); e.color(user.color()) - .author(|a| a.name(rank)) + .author(|a| a.name(&rank)) + .thumbnail(format!("https:{}", user.title_photo)) .title(&user.handle) + .url(user.profile_url()) .description(format!( "{}\n{}", if name == "" { @@ -46,7 +49,7 @@ pub fn user_embed<'a>(user: &User, e: &'a mut CreateEmbed) -> &'a mut CreateEmbe .field("Contribution", format!("**{}**", user.contribution), true) .field( "Rank", - format!("**{}** (max **{}**)", rank, max_rank), + format!("**{}** (max **{}**)", &rank, max_rank), false, ) } From 4b5dcfd0720133ff8dabc443ca6fd79ff0503f34 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 8 Feb 2020 18:37:17 -0500 Subject: [PATCH 04/10] 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 = { From 5b64d1e535571b7c7a338d7a892420b705baefd5 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sun, 9 Feb 2020 12:01:48 -0500 Subject: [PATCH 05/10] Create databases for cf --- Cargo.lock | 2 ++ youmubot-cf/Cargo.toml | 2 ++ youmubot-cf/src/db.rs | 26 ++++++++++++++++++++++++++ youmubot-cf/src/lib.rs | 13 ++++++++++++- youmubot/src/main.rs | 3 +++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 youmubot-cf/src/db.rs diff --git a/Cargo.lock b/Cargo.lock index 9746240..4a2e7ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1718,6 +1718,7 @@ name = "youmubot-cf" version = "0.1.0" dependencies = [ "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.10 (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)", @@ -1725,6 +1726,7 @@ 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)", "serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "youmubot-db 0.1.0", "youmubot-prelude 0.1.0", ] diff --git a/youmubot-cf/Cargo.toml b/youmubot-cf/Cargo.toml index aafd64c..a2d0583 100644 --- a/youmubot-cf/Cargo.toml +++ b/youmubot-cf/Cargo.toml @@ -14,5 +14,7 @@ codeforces = { git = "https://github.com/natsukagami/rust-codeforces-api" } regex = "1" lazy_static = "1" rayon = "1" +chrono = { version = "0.4", features = ["serde"] } youmubot-prelude = { path = "../youmubot-prelude" } +youmubot-db = { path = "../youmubot-db" } diff --git a/youmubot-cf/src/db.rs b/youmubot-cf/src/db.rs new file mode 100644 index 0000000..62404f8 --- /dev/null +++ b/youmubot-cf/src/db.rs @@ -0,0 +1,26 @@ +use chrono::{DateTime, Utc}; +use serenity::model::id::UserId; +use std::collections::HashMap; +use youmubot_db::DB; +use youmubot_prelude::*; + +/// A database map that stores an user with the respective handle. +pub type CfSavedUsers = DB>; + +/// A saved Codeforces user. +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] +pub struct CfUser { + pub handle: String, + pub last_update: DateTime, + pub rating: Option, +} + +impl Default for CfUser { + fn default() -> Self { + Self { + handle: "".to_owned(), + last_update: Utc::now(), + rating: None, + } + } +} diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 53f9604..7271ea2 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -3,15 +3,26 @@ use serenity::{ macros::{command, group}, Args, CommandResult, }, - model::{channel::Message, id::UserId}, + model::channel::Message, }; use youmubot_prelude::*; +mod db; mod embed; mod hook; +// /// Live-commentating a Codeforces round. +// pub mod live; + +pub use db::CfSavedUsers; pub use hook::codeforces_info_hook; +/// Sets up the CF databases. +pub fn setup(path: &std::path::Path, data: &mut ShareMap) { + CfSavedUsers::insert_into(data, path.join("cf_saved_users.yaml")) + .expect("Must be able to set up DB") +} + #[group] #[prefix = "cf"] #[description = "Codeforces-related commands"] diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index 8f372bc..20a31ae 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -81,6 +81,9 @@ fn main() { // osu! #[cfg(feature = "osu")] youmubot_osu::discord::setup(&db_path, &client, &mut data).expect("osu! is initialized"); + // codeforces + #[cfg(feature = "codeforces")] + youmubot_cf::setup(&db_path, &mut data); } #[cfg(feature = "core")] From adc33076f3024782f93ace0d21c469a761478236 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sun, 9 Feb 2020 14:13:19 -0500 Subject: [PATCH 06/10] Generalize UsernameArg --- youmubot-osu/src/discord/mod.rs | 64 +++++++++++---------------------- youmubot-prelude/src/args.rs | 29 +++++++++++++++ youmubot-prelude/src/lib.rs | 2 +- 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 70dadea..8bd76e2 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -180,39 +180,24 @@ impl FromStr for ModeArg { } } -enum UsernameArg { - Tagged(UserId), - Raw(String), +fn to_user_id_query( + s: Option, + data: &ShareMap, + msg: &Message, +) -> Result { + let id = match s { + Some(UsernameArg::Raw(s)) => return Ok(UserID::Auto(s)), + Some(UsernameArg::Tagged(r)) => r, + None => msg.author.id, + }; + + let db = OsuSavedUsers::open(data); + let db = db.borrow()?; + db.get(&id) + .cloned() + .map(|u| UserID::ID(u.id)) + .ok_or(Error::from("No saved account found")) } - -impl UsernameArg { - fn to_user_id_query(s: Option, data: &ShareMap, msg: &Message) -> Result { - let id = match s { - Some(UsernameArg::Raw(s)) => return Ok(UserID::Auto(s)), - Some(UsernameArg::Tagged(r)) => r, - None => msg.author.id, - }; - - let db = OsuSavedUsers::open(data); - let db = db.borrow()?; - db.get(&id) - .cloned() - .map(|u| UserID::ID(u.id)) - .ok_or(Error::from("No saved account found")) - } -} - -impl FromStr for UsernameArg { - type Err = String; - fn from_str(s: &str) -> Result { - match s.parse::() { - Ok(v) => Ok(UsernameArg::Tagged(v)), - Err(_) if !s.is_empty() => Ok(UsernameArg::Raw(s.to_owned())), - Err(_) => Err("username arg cannot be empty".to_owned()), - } - } -} - struct Nth(u8); impl FromStr for Nth { @@ -235,8 +220,7 @@ impl FromStr for Nth { pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { let nth = args.single::().unwrap_or(Nth(1)).0.min(50).max(1); let mode = args.single::().unwrap_or(ModeArg(Mode::Std)).0; - let user = - UsernameArg::to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; let osu = ctx.data.get_cloned::(); let user = osu @@ -308,11 +292,7 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult Some(bm) => { let b = &bm.0; let m = bm.1; - let user = UsernameArg::to_user_id_query( - args.single::().ok(), - &*ctx.data.read(), - msg, - )?; + let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; let osu = ctx.data.get_cloned::(); @@ -348,8 +328,7 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { .map(|ModeArg(t)| t) .unwrap_or(Mode::Std); - let user = - UsernameArg::to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; let osu = ctx.data.get_cloned::(); let user = osu @@ -387,8 +366,7 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { } fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult { - let user = - UsernameArg::to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; let osu = ctx.data.get_cloned::(); let user = osu.user(user, |f| f.mode(mode))?; match user { diff --git a/youmubot-prelude/src/args.rs b/youmubot-prelude/src/args.rs index d2e39b0..b6b96f9 100644 --- a/youmubot-prelude/src/args.rs +++ b/youmubot-prelude/src/args.rs @@ -1,4 +1,5 @@ pub use duration::Duration; +pub use username_arg::UsernameArg; mod duration { use std::fmt; @@ -169,3 +170,31 @@ mod duration { } } } + +mod username_arg { + use serenity::model::id::UserId; + use std::str::FromStr; + /// An argument that can be either a tagged user, or a raw string. + pub enum UsernameArg { + Tagged(UserId), + Raw(String), + } + + impl FromStr for UsernameArg { + type Err = String; + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(v) => Ok(UsernameArg::Tagged(v)), + Err(_) if !s.is_empty() => Ok(UsernameArg::Raw(s.to_owned())), + Err(_) => Err("username arg cannot be empty".to_owned()), + } + } + } + + impl UsernameArg { + /// Mention yourself. + pub fn mention>(v: T) -> Self { + Self::Tagged(v.into()) + } + } +} diff --git a/youmubot-prelude/src/lib.rs b/youmubot-prelude/src/lib.rs index 77a31a1..cf05cb0 100644 --- a/youmubot-prelude/src/lib.rs +++ b/youmubot-prelude/src/lib.rs @@ -8,7 +8,7 @@ pub mod reaction_watch; pub mod setup; pub use announcer::Announcer; -pub use args::Duration; +pub use args::{Duration, UsernameArg}; pub use pagination::Pagination; pub use reaction_watch::{ReactionHandler, ReactionWatcher}; From f512aa8726b607d7692cd28ffc6ef2682a86c8d5 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sun, 9 Feb 2020 14:14:15 -0500 Subject: [PATCH 07/10] Implement save command and user-aware profile command --- youmubot-cf/src/db.rs | 11 +++++++++ youmubot-cf/src/lib.rs | 56 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/youmubot-cf/src/db.rs b/youmubot-cf/src/db.rs index 62404f8..f5dc974 100644 --- a/youmubot-cf/src/db.rs +++ b/youmubot-cf/src/db.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, Utc}; +use codeforces::User; use serenity::model::id::UserId; use std::collections::HashMap; use youmubot_db::DB; @@ -24,3 +25,13 @@ impl Default for CfUser { } } } + +impl From for CfUser { + fn from(u: User) -> Self { + Self { + handle: u.handle, + last_update: Utc::now(), + rating: u.rating, + } + } +} diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 7271ea2..949692b 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -14,7 +14,8 @@ mod hook; // /// Live-commentating a Codeforces round. // pub mod live; -pub use db::CfSavedUsers; +use db::CfSavedUsers; + pub use hook::codeforces_info_hook; /// Sets up the CF databases. @@ -26,7 +27,7 @@ pub fn setup(path: &std::path::Path, data: &mut ShareMap) { #[group] #[prefix = "cf"] #[description = "Codeforces-related commands"] -#[commands(profile)] +#[commands(profile, save)] #[default_command(profile)] pub struct Codeforces; @@ -35,11 +36,28 @@ pub struct Codeforces; #[description = "Get an user's profile"] #[usage = "[handle or tag = yourself]"] #[example = "natsukagami"] -#[num_args(1)] +#[max_args(1)] pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { - let handle = args.single::()?; + let handle = args + .single::() + .unwrap_or(UsernameArg::mention(m.author.id)); let http = ctx.data.get_cloned::(); + 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(), + None => { + m.reply(&ctx, "no saved account found.")?; + return Ok(()); + } + } + } + }; + let account = codeforces::User::info(&http, &[&handle[..]])? .into_iter() .next(); @@ -57,3 +75,33 @@ pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult Ok(()) } + +#[command] +#[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 { + let handle = args.single::()?; + let http = ctx.data.get_cloned::(); + + let account = codeforces::User::info(&http, &[&handle[..]])? + .into_iter() + .next(); + + match account { + None => { + m.reply(&ctx, "cannot find an account with such handle")?; + } + Some(acc) => { + let db = CfSavedUsers::open(&*ctx.data.read()); + let mut db = db.borrow_mut()?; + m.reply( + &ctx, + format!("account `{}` has been linked to your account.", &acc.handle), + )?; + db.insert(m.author.id, acc.into()); + } + } + + Ok(()) +} From b0f70fb2a12e78026c3b27eca14762d9b8ca96a3 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sun, 9 Feb 2020 15:02:13 -0500 Subject: [PATCH 08/10] Implement server ranks --- youmubot-cf/src/lib.rs | 102 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 949692b..3f2fc96 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -1,9 +1,10 @@ use serenity::{ framework::standard::{ macros::{command, group}, - Args, CommandResult, + Args, CommandError as Error, CommandResult, }, model::channel::Message, + utils::MessageBuilder, }; use youmubot_prelude::*; @@ -27,7 +28,7 @@ pub fn setup(path: &std::path::Path, data: &mut ShareMap) { #[group] #[prefix = "cf"] #[description = "Codeforces-related commands"] -#[commands(profile, save)] +#[commands(profile, save, ranks)] #[default_command(profile)] pub struct Codeforces; @@ -105,3 +106,100 @@ pub fn save(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { Ok(()) } + +#[command] +#[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 { + let everyone = { + let db = CfSavedUsers::open(&*ctx.data.read()); + let db = db.borrow()?; + db.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>() + }; + 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::>(); + 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.")?; + return Ok(()); + } + + 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, + |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]; + + 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("```"); + + // 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; + 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 + )); + } + + m.push_line("```"); + m.push(format!( + "Page **{}/{}**. Last updated **{}**", + page + 1, + total_pages, + last_updated.to_rfc2822() + )); + + (e.content(m.build()), Ok(())) + }, + std::time::Duration::from_secs(60), + )?; + + Ok(()) +} From 5ae8ca5f0e278abc270ee3f9925b891ffb6f8537 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sun, 9 Feb 2020 15:08:18 -0500 Subject: [PATCH 09/10] Signal on the end of pagination handler cycle --- youmubot-prelude/src/pagination.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index e41944d..77ed411 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -117,6 +117,12 @@ impl PaginationHandler { } } +impl Drop for PaginationHandler { + fn drop(&mut self) { + self.message.react(&self.ctx, "🛑").ok(); + } +} + impl ReactionHandler for PaginationHandler { fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult { if reaction.message_id != self.message.id { From 3a14c401cd38e8973d16844f0d5ce9be9e054989 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Mon, 10 Feb 2020 15:18:22 -0500 Subject: [PATCH 10/10] Implement Codeforces announcer --- youmubot-cf/src/announcer.rs | 96 ++++++++++++++++++++++++++++++++++++ youmubot-cf/src/embed.rs | 12 ++++- youmubot-cf/src/lib.rs | 6 ++- youmubot/src/main.rs | 2 +- 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 youmubot-cf/src/announcer.rs diff --git a/youmubot-cf/src/announcer.rs b/youmubot-cf/src/announcer.rs new file mode 100644 index 0000000..d1ce197 --- /dev/null +++ b/youmubot-cf/src/announcer.rs @@ -0,0 +1,96 @@ +use crate::db::{CfSavedUsers, CfUser}; +use announcer::MemberToChannels; +use chrono::{DateTime, Utc}; +use codeforces::{RatingChange, User}; +use serenity::{ + framework::standard::{CommandError, CommandResult}, + http::CacheHttp, + model::id::{ChannelId, UserId}, + CacheAndHttp, +}; +use std::sync::Arc; +use youmubot_prelude::*; + +type Reqwest = ::Value; + +/// Updates the rating and rating changes of the users. +pub fn updates( + http: Arc, + data: AppData, + channels: MemberToChannels, +) -> CommandResult { + let mut users = CfSavedUsers::open(&*data.read()).borrow()?.clone(); + let reqwest = data.get_cloned::(); + + for (user_id, cfu) in users.iter_mut() { + if let Err(e) = update_user(http.clone(), &channels, &reqwest, *user_id, cfu) { + dbg!((*user_id, e)); + } + } + + *CfSavedUsers::open(&*data.read()).borrow_mut()? = users; + Ok(()) +} + +fn update_user( + http: Arc, + channels: &MemberToChannels, + reqwest: &Reqwest, + user_id: UserId, + cfu: &mut CfUser, +) -> CommandResult { + let info = User::info(reqwest, &[cfu.handle.as_str()])? + .into_iter() + .next() + .ok_or(CommandError::from("Not found"))?; + + let rating_changes = { + let mut v = info.rating_changes(reqwest)?; + v.reverse(); + v + }; + + let mut channels_list: Option> = None; + let last_update = std::mem::replace(&mut cfu.last_update, Utc::now()); + // Update the rating + cfu.rating = info.rating; + + let mut send_message = |rc: RatingChange| -> CommandResult { + let (contest, _, _) = + codeforces::Contest::standings(reqwest, rc.contest_id, |f| f.limit(1, 1))?; + let channels = + channels_list.get_or_insert_with(|| channels.channels_of(http.clone(), user_id)); + for channel in channels { + if let Err(e) = channel.send_message(http.http(), |e| { + e.content(format!("Rating change for {}!", user_id.mention())) + .embed(|c| { + crate::embed::rating_change_embed( + &rc, + &info, + &contest, + &user_id.mention(), + c, + ) + }) + }) { + dbg!(e); + } + } + Ok(()) + }; + + // Check for any good announcements to make + for rc in rating_changes { + let date: DateTime = DateTime::from_utc( + chrono::NaiveDateTime::from_timestamp(rc.rating_update_time_seconds as i64, 0), + Utc, + ); + if &date > &last_update { + if let Err(v) = send_message(rc) { + dbg!(v); + } + } + } + + Ok(()) +} diff --git a/youmubot-cf/src/embed.rs b/youmubot-cf/src/embed.rs index be81961..2531029 100644 --- a/youmubot-cf/src/embed.rs +++ b/youmubot-cf/src/embed.rs @@ -64,7 +64,7 @@ pub fn rating_change_embed<'a>( ) -> &'a mut CreateEmbed { let delta = (rating_change.new_rating as i64) - (rating_change.old_rating as i64); let color = if delta < 0 { 0xff0000 } else { 0x00ff00 }; - let message = if delta < 0 { + let message = if delta > 0 { MessageBuilder::new() .push(tag) .push(" competed in ") @@ -89,11 +89,19 @@ pub fn rating_change_embed<'a>( }; e.author(|a| { - a.icon_url(&user.avatar) + a.icon_url(format!("http:{}", &user.avatar)) .url(user.profile_url()) .name(&user.handle) }) .color(color) .description(message) .field("Contest Link", contest.url(), true) + .field( + "Rating Change", + format!( + "from **{}** to **{}**", + rating_change.old_rating, rating_change.new_rating + ), + false, + ) } diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 3f2fc96..3abb68e 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -8,6 +8,7 @@ use serenity::{ }; use youmubot_prelude::*; +mod announcer; mod db; mod embed; mod hook; @@ -20,9 +21,10 @@ use db::CfSavedUsers; pub use hook::codeforces_info_hook; /// Sets up the CF databases. -pub fn setup(path: &std::path::Path, data: &mut ShareMap) { +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") + .expect("Must be able to set up DB"); + announcers.add("codeforces", announcer::updates); } #[group] diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index 88b813b..307b490 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -87,7 +87,7 @@ fn main() { .expect("osu! is initialized"); // codeforces #[cfg(feature = "codeforces")] - youmubot_cf::setup(&db_path, &mut data); + youmubot_cf::setup(&db_path, &mut data, &mut announcers); } #[cfg(feature = "core")]