Merge branch 'codeforces' so far

This commit is contained in:
Natsu Kagami 2020-02-10 15:18:52 -05:00
commit 4b0c7bf4b4
Signed by: nki
GPG key ID: 73376E117CD20735
14 changed files with 721 additions and 45 deletions

39
Cargo.lock generated
View file

@ -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"
@ -151,6 +160,16 @@ 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#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]]
name = "command_attr"
version = "0.1.7"
@ -1687,12 +1706,30 @@ 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",
"youmubot-prelude 0.1.0",
]
[[package]]
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)",
"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)",
"youmubot-db 0.1.0",
"youmubot-prelude 0.1.0",
]
[[package]]
name = "youmubot-core"
version = "0.1.0"
@ -1747,6 +1784,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"
@ -1769,6 +1807,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)" = "<none>"
"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"

View file

@ -4,6 +4,7 @@ members = [
"youmubot-prelude",
"youmubot-db",
"youmubot-core",
"youmubot-cf",
"youmubot-osu",
"youmubot",
]

20
youmubot-cf/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "youmubot-cf"
version = "0.1.0"
authors = ["Natsu Kagami <natsukagami@gmail.com>"]
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"
Inflector = "0.11"
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" }

View file

@ -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 = <HTTPClient as TypeMapKey>::Value;
/// Updates the rating and rating changes of the users.
pub fn updates(
http: Arc<CacheAndHttp>,
data: AppData,
channels: MemberToChannels,
) -> CommandResult {
let mut users = CfSavedUsers::open(&*data.read()).borrow()?.clone();
let reqwest = data.get_cloned::<HTTPClient>();
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<CacheAndHttp>,
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<Vec<ChannelId>> = 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<Utc> = 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(())
}

37
youmubot-cf/src/db.rs Normal file
View file

@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};
use codeforces::User;
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<HashMap<UserId, CfUser>>;
/// A saved Codeforces user.
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct CfUser {
pub handle: String,
pub last_update: DateTime<Utc>,
pub rating: Option<i64>,
}
impl Default for CfUser {
fn default() -> Self {
Self {
handle: "".to_owned(),
last_update: Utc::now(),
rating: None,
}
}
}
impl From<User> for CfUser {
fn from(u: User) -> Self {
Self {
handle: u.handle,
last_update: Utc::now(),
rating: u.rating,
}
}
}

107
youmubot-cf/src/embed.rs Normal file
View file

@ -0,0 +1,107 @@
use codeforces::{Contest, RatingChange, User};
use inflector::Inflector;
use serenity::{builder::CreateEmbed, utils::MessageBuilder};
use std::borrow::Borrow;
fn unwrap_or_ref<'a, T: ?Sized, B: Borrow<T>>(opt: &'a Option<B>, 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").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]
.iter()
.filter_map(|v| v.as_ref().map(|v| v.as_str()))
.collect::<Vec<_>>()
.join(" ");
let place = &[&user.organization, &user.city, &user.country]
.iter()
.filter_map(|v| v.as_ref().map(|v| v.as_str()))
.collect::<Vec<_>>()
.join(", ");
e.color(user.color())
.author(|a| a.name(&rank))
.thumbnail(format!("https:{}", user.title_photo))
.title(&user.handle)
.url(user.profile_url())
.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(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,
)
}

145
youmubot-cf/src/hook.rs Normal file
View file

@ -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<contest>\d+)(?:/problem/(?P<problem>\w+))?"
)
.unwrap();
static ref PROBLEMSET_LINK: Regex = Regex::new(
r"https?://codeforces\.com/problemset/problem/(?P<contest>\d+)/(?P<problem>\w+)"
)
.unwrap();
}
enum ContestOrProblem {
Contest(Contest, Vec<Problem>),
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::<HTTPClient>();
let matches = CONTEST_LINK
.captures_iter(&m.content)
.chain(PROBLEMSET_LINK.captures_iter(&m.content))
// .collect::<Vec<_>>()
// .into_par_iter()
.filter_map(|v| match parse_capture(http.clone(), v) {
Ok(v) => Some(v),
Err(e) => {
dbg!(e);
None
}
})
.collect::<Vec<_>>();
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: <HTTPClient as TypeMapKey>::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(),
)),
}
}

207
youmubot-cf/src/lib.rs Normal file
View file

@ -0,0 +1,207 @@
use serenity::{
framework::standard::{
macros::{command, group},
Args, CommandError as Error, CommandResult,
},
model::channel::Message,
utils::MessageBuilder,
};
use youmubot_prelude::*;
mod announcer;
mod db;
mod embed;
mod hook;
// /// Live-commentating a Codeforces round.
// pub mod live;
use db::CfSavedUsers;
pub use hook::codeforces_info_hook;
/// Sets up the CF databases.
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");
announcers.add("codeforces", announcer::updates);
}
#[group]
#[prefix = "cf"]
#[description = "Codeforces-related commands"]
#[commands(profile, save, ranks)]
#[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"]
#[max_args(1)]
pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult {
let handle = args
.single::<UsernameArg>()
.unwrap_or(UsernameArg::mention(m.author.id));
let http = ctx.data.get_cloned::<HTTPClient>();
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();
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(())
}
#[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::<String>()?;
let http = ctx.data.get_cloned::<HTTPClient>();
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(())
}
#[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::<Vec<_>>()
};
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::<Vec<_>>();
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::<ReactionWatcher>().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(())
}

View file

@ -179,13 +179,11 @@ impl FromStr for ModeArg {
}
}
enum UsernameArg {
Tagged(UserId),
Raw(String),
}
impl UsernameArg {
fn to_user_id_query(s: Option<Self>, data: &ShareMap, msg: &Message) -> Result<UserID, Error> {
fn to_user_id_query(
s: Option<UsernameArg>,
data: &ShareMap,
msg: &Message,
) -> Result<UserID, Error> {
let id = match s {
Some(UsernameArg::Raw(s)) => return Ok(UserID::Auto(s)),
Some(UsernameArg::Tagged(r)) => r,
@ -199,19 +197,6 @@ impl UsernameArg {
.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<Self, Self::Err> {
match s.parse::<UserId>() {
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 {
@ -234,8 +219,7 @@ impl FromStr for Nth {
pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
let nth = args.single::<Nth>().unwrap_or(Nth(1)).0.min(50).max(1);
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user =
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
let osu = ctx.data.get_cloned::<OsuClient>();
let user = osu
@ -307,11 +291,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::<UsernameArg>().ok(),
&*ctx.data.read(),
msg,
)?;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
let osu = ctx.data.get_cloned::<OsuClient>();
@ -347,8 +327,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::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
let osu = ctx.data.get_cloned::<OsuClient>();
let user = osu
@ -386,8 +365,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::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
let osu = ctx.data.get_cloned::<OsuClient>();
let user = osu.user(user, |f| f.mode(mode))?;
match user {

View file

@ -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<Self, Self::Err> {
match s.parse::<UserId>() {
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<T: Into<UserId>>(v: T) -> Self {
Self::Tagged(v.into())
}
}
}

View file

@ -8,7 +8,7 @@ pub mod reaction_watch;
pub mod setup;
pub use announcer::{Announcer, AnnouncerHandler};
pub use args::Duration;
pub use args::{Duration, UsernameArg};
pub use pagination::Pagination;
pub use reaction_watch::{ReactionHandler, ReactionWatcher};

View file

@ -117,6 +117,12 @@ impl<T: Pagination> PaginationHandler<T> {
}
}
impl<T: Pagination> Drop for PaginationHandler<T> {
fn drop(&mut self) {
self.message.react(&self.ctx, "🛑").ok();
}
}
impl<T: Pagination> ReactionHandler for PaginationHandler<T> {
fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult {
if reaction.message_id != self.message.id {

View file

@ -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 }

View file

@ -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 = {
@ -83,12 +85,17 @@ fn main() {
#[cfg(feature = "osu")]
youmubot_osu::discord::setup(&db_path, &mut data, &mut announcers)
.expect("osu! is initialized");
// codeforces
#[cfg(feature = "codeforces")]
youmubot_cf::setup(&db_path, &mut data, &mut announcers);
}
#[cfg(feature = "core")]
println!("Core enabled.");
#[cfg(feature = "osu")]
println!("osu! enabled.");
#[cfg(feature = "codeforces")]
println!("codeforces enabled.");
client.with_framework(fw);
announcers.scan(std::time::Duration::from_secs(300));
@ -175,5 +182,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
}