mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-18 16:28:55 +00:00
Merge branch 'codeforces' so far
This commit is contained in:
commit
4b0c7bf4b4
14 changed files with 721 additions and 45 deletions
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -1,5 +1,14 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# 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]]
|
[[package]]
|
||||||
name = "adler32"
|
name = "adler32"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -151,6 +160,16 @@ dependencies = [
|
||||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "command_attr"
|
name = "command_attr"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
@ -1687,12 +1706,30 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"youmubot-cf 0.1.0",
|
||||||
"youmubot-core 0.1.0",
|
"youmubot-core 0.1.0",
|
||||||
"youmubot-db 0.1.0",
|
"youmubot-db 0.1.0",
|
||||||
"youmubot-osu 0.1.0",
|
"youmubot-osu 0.1.0",
|
||||||
"youmubot-prelude 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]]
|
[[package]]
|
||||||
name = "youmubot-core"
|
name = "youmubot-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1747,6 +1784,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[metadata]
|
[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 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 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"
|
"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 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 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 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 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 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"
|
"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
|
||||||
|
|
|
@ -4,6 +4,7 @@ members = [
|
||||||
"youmubot-prelude",
|
"youmubot-prelude",
|
||||||
"youmubot-db",
|
"youmubot-db",
|
||||||
"youmubot-core",
|
"youmubot-core",
|
||||||
|
"youmubot-cf",
|
||||||
"youmubot-osu",
|
"youmubot-osu",
|
||||||
"youmubot",
|
"youmubot",
|
||||||
]
|
]
|
||||||
|
|
20
youmubot-cf/Cargo.toml
Normal file
20
youmubot-cf/Cargo.toml
Normal 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" }
|
96
youmubot-cf/src/announcer.rs
Normal file
96
youmubot-cf/src/announcer.rs
Normal 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
37
youmubot-cf/src/db.rs
Normal 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
107
youmubot-cf/src/embed.rs
Normal 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
145
youmubot-cf/src/hook.rs
Normal 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
207
youmubot-cf/src/lib.rs
Normal 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(())
|
||||||
|
}
|
|
@ -179,39 +179,24 @@ impl FromStr for ModeArg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UsernameArg {
|
fn to_user_id_query(
|
||||||
Tagged(UserId),
|
s: Option<UsernameArg>,
|
||||||
Raw(String),
|
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,
|
||||||
|
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<Self>, 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,
|
|
||||||
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<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);
|
struct Nth(u8);
|
||||||
|
|
||||||
impl FromStr for Nth {
|
impl FromStr for Nth {
|
||||||
|
@ -234,8 +219,7 @@ impl FromStr for Nth {
|
||||||
pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
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 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 mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
|
||||||
let user =
|
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
|
||||||
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let user = osu
|
let user = osu
|
||||||
|
@ -307,11 +291,7 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
Some(bm) => {
|
Some(bm) => {
|
||||||
let b = &bm.0;
|
let b = &bm.0;
|
||||||
let m = bm.1;
|
let m = bm.1;
|
||||||
let user = UsernameArg::to_user_id_query(
|
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
args.single::<UsernameArg>().ok(),
|
|
||||||
&*ctx.data.read(),
|
|
||||||
msg,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
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)
|
.map(|ModeArg(t)| t)
|
||||||
.unwrap_or(Mode::Std);
|
.unwrap_or(Mode::Std);
|
||||||
|
|
||||||
let user =
|
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
|
||||||
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let user = osu
|
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 {
|
fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
||||||
let user =
|
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let user = osu.user(user, |f| f.mode(mode))?;
|
let user = osu.user(user, |f| f.mode(mode))?;
|
||||||
match user {
|
match user {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub use duration::Duration;
|
pub use duration::Duration;
|
||||||
|
pub use username_arg::UsernameArg;
|
||||||
|
|
||||||
mod duration {
|
mod duration {
|
||||||
use std::fmt;
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ pub mod reaction_watch;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
pub use announcer::{Announcer, AnnouncerHandler};
|
pub use announcer::{Announcer, AnnouncerHandler};
|
||||||
pub use args::Duration;
|
pub use args::{Duration, UsernameArg};
|
||||||
pub use pagination::Pagination;
|
pub use pagination::Pagination;
|
||||||
pub use reaction_watch::{ReactionHandler, ReactionWatcher};
|
pub use reaction_watch::{ReactionHandler, ReactionWatcher};
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
impl<T: Pagination> ReactionHandler for PaginationHandler<T> {
|
||||||
fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult {
|
fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult {
|
||||||
if reaction.message_id != self.message.id {
|
if reaction.message_id != self.message.id {
|
||||||
|
|
|
@ -6,9 +6,10 @@ edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[features]
|
[features]
|
||||||
default = ["core", "osu"]
|
default = ["core", "osu", "codeforces"]
|
||||||
core = []
|
core = []
|
||||||
osu = ["youmubot-osu"]
|
osu = ["youmubot-osu"]
|
||||||
|
codeforces = ["youmubot-cf"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serenity = "0.8"
|
serenity = "0.8"
|
||||||
|
@ -17,4 +18,5 @@ youmubot-db = { path = "../youmubot-db" }
|
||||||
youmubot-prelude = { path = "../youmubot-prelude" }
|
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||||
youmubot-core = { path = "../youmubot-core" }
|
youmubot-core = { path = "../youmubot-core" }
|
||||||
youmubot-osu = { path = "../youmubot-osu", optional = true }
|
youmubot-osu = { path = "../youmubot-osu", optional = true }
|
||||||
|
youmubot-cf = { path = "../youmubot-cf", optional = true }
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,8 @@ fn main() {
|
||||||
// Set up hooks
|
// Set up hooks
|
||||||
#[cfg(feature = "osu")]
|
#[cfg(feature = "osu")]
|
||||||
handler.hooks.push(youmubot_osu::discord::hook);
|
handler.hooks.push(youmubot_osu::discord::hook);
|
||||||
|
#[cfg(feature = "codeforces")]
|
||||||
|
handler.hooks.push(youmubot_cf::codeforces_info_hook);
|
||||||
|
|
||||||
// Sets up a client
|
// Sets up a client
|
||||||
let mut client = {
|
let mut client = {
|
||||||
|
@ -83,12 +85,17 @@ fn main() {
|
||||||
#[cfg(feature = "osu")]
|
#[cfg(feature = "osu")]
|
||||||
youmubot_osu::discord::setup(&db_path, &mut data, &mut announcers)
|
youmubot_osu::discord::setup(&db_path, &mut data, &mut announcers)
|
||||||
.expect("osu! is initialized");
|
.expect("osu! is initialized");
|
||||||
|
// codeforces
|
||||||
|
#[cfg(feature = "codeforces")]
|
||||||
|
youmubot_cf::setup(&db_path, &mut data, &mut announcers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "core")]
|
#[cfg(feature = "core")]
|
||||||
println!("Core enabled.");
|
println!("Core enabled.");
|
||||||
#[cfg(feature = "osu")]
|
#[cfg(feature = "osu")]
|
||||||
println!("osu! enabled.");
|
println!("osu! enabled.");
|
||||||
|
#[cfg(feature = "codeforces")]
|
||||||
|
println!("codeforces enabled.");
|
||||||
|
|
||||||
client.with_framework(fw);
|
client.with_framework(fw);
|
||||||
announcers.scan(std::time::Duration::from_secs(300));
|
announcers.scan(std::time::Duration::from_secs(300));
|
||||||
|
@ -175,5 +182,7 @@ fn setup_framework(client: &Client) -> StandardFramework {
|
||||||
.group(&youmubot_core::COMMUNITY_GROUP);
|
.group(&youmubot_core::COMMUNITY_GROUP);
|
||||||
#[cfg(feature = "osu")]
|
#[cfg(feature = "osu")]
|
||||||
let fw = fw.group(&youmubot_osu::discord::OSU_GROUP);
|
let fw = fw.group(&youmubot_osu::discord::OSU_GROUP);
|
||||||
|
#[cfg(feature = "codeforces")]
|
||||||
|
let fw = fw.group(&youmubot_cf::CODEFORCES_GROUP);
|
||||||
fw
|
fw
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue