Parsing and printing the basic info with old links

This commit is contained in:
Natsu Kagami 2019-12-24 14:42:56 -05:00
parent 0cc9b318ad
commit e37ae361fd
13 changed files with 422 additions and 54 deletions

25
Cargo.lock generated
View file

@ -5,6 +5,14 @@ name = "adler32"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "aho-corasick"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.25" version = "1.0.25"
@ -1000,7 +1008,10 @@ name = "regex"
version = "1.3.1" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -1323,6 +1334,14 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "thread_local"
version = "0.3.6"
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)",
]
[[package]] [[package]]
name = "threadpool" name = "threadpool"
version = "1.7.1" version = "1.7.1"
@ -1802,12 +1821,15 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)",
"rustbreak 2.0.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)", "rustbreak 2.0.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"serenity 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "serenity 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"youmubot-osu 0.1.0",
] ]
[[package]] [[package]]
@ -1817,11 +1839,13 @@ dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
"serenity 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "serenity 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[metadata] [metadata]
"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 anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9267dff192e68f3399525901e709a48c1d3982c9c072fa32f2127a0cb0babf14" "checksum anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9267dff192e68f3399525901e709a48c1d3982c9c072fa32f2127a0cb0babf14"
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea" "checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea"
@ -1972,6 +1996,7 @@ dependencies = [
"checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238" "checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238"
"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865" "checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" "checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"

View file

@ -11,3 +11,6 @@ serenity = "0.7"
chrono = "0.4.10" chrono = "0.4.10"
reqwest = "0.9.24" reqwest = "0.9.24"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
serde_json = "1"

View file

@ -2,6 +2,9 @@ pub mod models;
pub mod request; pub mod request;
#[cfg(test)]
mod test;
use models::*; use models::*;
use request::builders::*; use request::builders::*;
use request::*; use request::*;
@ -25,13 +28,11 @@ impl Client {
&self, &self,
client: &HTTPClient, client: &HTTPClient,
kind: BeatmapRequestKind, kind: BeatmapRequestKind,
f: impl FnOnce(BeatmapRequestBuilder) -> BeatmapRequestBuilder, f: impl FnOnce(&mut BeatmapRequestBuilder) -> &mut BeatmapRequestBuilder,
) -> Result<Vec<Beatmap>, Error> { ) -> Result<Vec<Beatmap>, Error> {
let res = f(BeatmapRequestBuilder::new(kind)) let mut r = BeatmapRequestBuilder::new(kind);
.build(client) f(&mut r);
.query(&[("k", &self.key)]) let res = r.build(client).query(&[("k", &self.key)]).send()?.json()?;
.send()?
.json()?;
Ok(res) Ok(res)
} }
} }

View file

@ -25,15 +25,15 @@ impl<'de> Deserialize<'de> for Beatmap {
bpm: parse_from_str(&raw.bpm)?, bpm: parse_from_str(&raw.bpm)?,
creator: raw.creator, creator: raw.creator,
creator_id: parse_from_str(&raw.creator_id)?, creator_id: parse_from_str(&raw.creator_id)?,
source: raw.source, source: raw.source.filter(|v| !v.is_empty()),
genre: parse_genre(&raw.genre_id)?, genre: parse_genre(&raw.genre_id)?,
language: parse_language(&raw.language_id)?, language: parse_language(&raw.language_id)?,
tags: raw.tags.split_whitespace().map(|v| v.to_owned()).collect(), tags: raw.tags.split_whitespace().map(|v| v.to_owned()).collect(),
difficulty_name: raw.version, difficulty_name: raw.version,
difficulty: Difficulty { difficulty: Difficulty {
stars: parse_from_str(&raw.difficultyrating)?, stars: parse_from_str(&raw.difficultyrating)?,
aim: parse_from_str(&raw.diff_aim)?, aim: raw.diff_aim.map(parse_from_str).transpose()?,
speed: parse_from_str(&raw.diff_speed)?, speed: raw.diff_speed.map(parse_from_str).transpose()?,
cs: parse_from_str(&raw.diff_size)?, cs: parse_from_str(&raw.diff_size)?,
od: parse_from_str(&raw.diff_overall)?, od: parse_from_str(&raw.diff_overall)?,
ar: parse_from_str(&raw.diff_approach)?, ar: parse_from_str(&raw.diff_approach)?,
@ -41,7 +41,7 @@ impl<'de> Deserialize<'de> for Beatmap {
count_normal: parse_from_str(&raw.count_normal)?, count_normal: parse_from_str(&raw.count_normal)?,
count_slider: parse_from_str(&raw.count_slider)?, count_slider: parse_from_str(&raw.count_slider)?,
count_spinner: parse_from_str(&raw.count_spinner)?, count_spinner: parse_from_str(&raw.count_spinner)?,
max_combo: parse_from_str(&raw.max_combo)?, max_combo: raw.max_combo.map(parse_from_str).transpose()?,
}, },
drain_length: parse_duration(&raw.hit_length)?, drain_length: parse_duration(&raw.hit_length)?,
total_length: parse_duration(&raw.total_length)?, total_length: parse_duration(&raw.total_length)?,
@ -61,8 +61,8 @@ fn parse_mode<E: de::Error>(s: impl AsRef<str>) -> Result<Mode, E> {
Ok(match t { Ok(match t {
0 => Std, 0 => Std,
1 => Taiko, 1 => Taiko,
2 => Mania, 2 => Catch,
3 => Catch, 3 => Mania,
_ => return Err(E::custom(format!("invalid value {} for mode", t))), _ => return Err(E::custom(format!("invalid value {} for mode", t))),
}) })
} }
@ -83,7 +83,7 @@ fn parse_language<E: de::Error>(s: impl AsRef<str>) -> Result<Language, E> {
9 => Swedish, 9 => Swedish,
10 => Spanish, 10 => Spanish,
11 => Italian, 11 => Italian,
_ => return Err(E::custom(format!("Invalid value {} for language", t))), _ => return Err(E::custom(format!("invalid value {} for language", t))),
}) })
} }
@ -101,7 +101,7 @@ fn parse_genre<E: de::Error>(s: impl AsRef<str>) -> Result<Genre, E> {
7 => Novelty, 7 => Novelty,
9 => HipHop, 9 => HipHop,
10 => Electronic, 10 => Electronic,
_ => return Err(E::custom(format!("Invalid value {} for genre", t))), _ => return Err(E::custom(format!("invalid value {} for genre", t))),
}) })
} }
@ -110,14 +110,14 @@ fn parse_duration<E: de::Error>(s: impl AsRef<str>) -> Result<Duration, E> {
} }
fn parse_from_str<T: FromStr, E: de::Error>(s: impl AsRef<str>) -> Result<T, E> { fn parse_from_str<T: FromStr, E: de::Error>(s: impl AsRef<str>) -> Result<T, E> {
T::from_str(s.as_ref()).map_err(|_| E::custom(format!("Invalid value {}", s.as_ref()))) T::from_str(s.as_ref()).map_err(|_| E::custom(format!("invalid value {}", s.as_ref())))
} }
fn parse_bool<E: de::Error>(b: impl AsRef<str>) -> Result<bool, E> { fn parse_bool<E: de::Error>(b: impl AsRef<str>) -> Result<bool, E> {
match b.as_ref() { match b.as_ref() {
"1" => Ok(true), "1" => Ok(true),
"0" => Ok(false), "0" => Ok(false),
_ => Err(E::custom("Invalid value for bool")), _ => Err(E::custom("invalid value for bool")),
} }
} }
@ -127,11 +127,15 @@ fn parse_approval_status<E: de::Error>(b: &raw::Beatmap) -> Result<ApprovalStatu
"4" => Loved, "4" => Loved,
"3" => Qualified, "3" => Qualified,
"2" => Approved, "2" => Approved,
"1" => Ranked(parse_date(&b.approved_date)?), "1" => Ranked(parse_date(
b.approved_date
.as_ref()
.ok_or(E::custom("expected approved date got none"))?,
)?),
"0" => Pending, "0" => Pending,
"-1" => WIP, "-1" => WIP,
"-2" => Graveyarded, "-2" => Graveyarded,
_ => return Err(E::custom("Invalid value for approval status")), _ => return Err(E::custom("invalid value for approval status")),
}) })
} }
@ -142,15 +146,15 @@ fn parse_date<E: de::Error>(date: impl AsRef<str>) -> Result<DateTime<Utc>, E> {
date.as_ref(), date.as_ref(),
(&[ (&[
Item::Numeric(Numeric::Year, Pad::Zero), Item::Numeric(Numeric::Year, Pad::Zero),
Item::Literal("/"), Item::Literal("-"),
Item::Numeric(Numeric::Month, Pad::Zero), Item::Numeric(Numeric::Month, Pad::Zero),
Item::Literal("/"), Item::Literal("-"),
Item::Numeric(Numeric::Day, Pad::Zero), Item::Numeric(Numeric::Day, Pad::Zero),
Item::Space(""), Item::Space(""),
Item::Numeric(Numeric::Hour, Pad::Zero), Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Literal("-"), Item::Literal(":"),
Item::Numeric(Numeric::Minute, Pad::Zero), Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Literal("-"), Item::Literal(":"),
Item::Numeric(Numeric::Second, Pad::Zero), Item::Numeric(Numeric::Second, Pad::Zero),
]) ])
.iter(), .iter(),

View file

@ -18,8 +18,8 @@ pub enum ApprovalStatus {
#[derive(Debug)] #[derive(Debug)]
pub struct Difficulty { pub struct Difficulty {
pub stars: f64, pub stars: f64,
pub aim: f64, pub aim: Option<f64>,
pub speed: f64, pub speed: Option<f64>,
pub cs: f64, pub cs: f64,
pub od: f64, pub od: f64,
@ -29,7 +29,7 @@ pub struct Difficulty {
pub count_normal: u64, pub count_normal: u64,
pub count_slider: u64, pub count_slider: u64,
pub count_spinner: u64, pub count_spinner: u64,
pub max_combo: u64, pub max_combo: Option<u64>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -61,17 +61,27 @@ pub enum Language {
Spanish, Spanish,
Italian, Italian,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mode { pub enum Mode {
Std, Std,
Taiko, Taiko,
Mania,
Catch, Catch,
Mania,
} }
impl ToString for Mode { impl std::fmt::Display for Mode {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
(*self as u64).to_string() use Mode::*;
write!(
f,
"{}",
match self {
Std => "osu!",
Taiko => "osu!taiko",
Mania => "osu!mania",
Catch => "osu!catch",
}
)
} }
} }

View file

@ -1,10 +1,10 @@
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
pub(crate) struct Beatmap { pub(crate) struct Beatmap {
pub approved: String, pub approved: String,
pub submit_date: String, pub submit_date: String,
pub approved_date: String, pub approved_date: Option<String>,
pub last_update: String, pub last_update: String,
pub artist: String, pub artist: String,
pub beatmap_id: String, pub beatmap_id: String,
@ -13,8 +13,8 @@ pub(crate) struct Beatmap {
pub creator: String, pub creator: String,
pub creator_id: String, pub creator_id: String,
pub difficultyrating: String, pub difficultyrating: String,
pub diff_aim: String, pub diff_aim: Option<String>,
pub diff_speed: String, pub diff_speed: Option<String>,
pub diff_size: String, pub diff_size: String,
pub diff_overall: String, pub diff_overall: String,
pub diff_approach: String, pub diff_approach: String,
@ -36,7 +36,7 @@ pub(crate) struct Beatmap {
pub count_normal: String, pub count_normal: String,
pub count_slider: String, pub count_slider: String,
pub count_spinner: String, pub count_spinner: String,
pub max_combo: String, pub max_combo: Option<String>,
pub download_unavailable: String, pub download_unavailable: String,
pub audio_unavailable: String, pub audio_unavailable: String,
} }

View file

@ -15,9 +15,12 @@ impl<T: ToQuery> ToQuery for Option<T> {
} }
} }
impl ToQuery for Mode { impl ToQuery for (Mode, bool) {
fn to_query(&self) -> Vec<(&'static str, String)> { fn to_query(&self) -> Vec<(&'static str, String)> {
vec![("m", (*self as u8).to_string())] vec![
("m", (self.0 as u8).to_string()),
("a", (self.1 as u8).to_string()),
]
} }
} }
@ -74,27 +77,24 @@ pub mod builders {
pub struct BeatmapRequestBuilder { pub struct BeatmapRequestBuilder {
kind: BeatmapRequestKind, kind: BeatmapRequestKind,
since: Option<DateTime<Utc>>, since: Option<DateTime<Utc>>,
mode: Mode, mode: Option<(Mode, /* Converted */ bool)>,
converted: bool,
} }
impl BeatmapRequestBuilder { impl BeatmapRequestBuilder {
pub(crate) fn new(kind: BeatmapRequestKind) -> Self { pub(crate) fn new(kind: BeatmapRequestKind) -> Self {
BeatmapRequestBuilder { BeatmapRequestBuilder {
kind, kind,
since: None, since: None,
mode: Mode::Std, mode: None,
converted: false,
} }
} }
pub fn since(&mut self, since: DateTime<Utc>) -> &Self { pub fn since(&mut self, since: DateTime<Utc>) -> &mut Self {
self.since = Some(since); self.since = Some(since);
self self
} }
pub fn mode(&mut self, mode: Mode, converted: bool) -> &Self { pub fn mode(&mut self, mode: Mode, converted: bool) -> &mut Self {
self.mode = mode; self.mode = Some((mode, converted));
self.converted = converted;
self self
} }
@ -104,14 +104,6 @@ pub mod builders {
.query(&self.kind.to_query()) .query(&self.kind.to_query())
.query(&self.since.map(|v| ("since", v)).to_query()) .query(&self.since.map(|v| ("since", v)).to_query())
.query(&self.mode.to_query()) .query(&self.mode.to_query())
.query(
&(if self.converted {
Some(("a", "1".to_owned()))
} else {
None
})
.to_query(),
)
} }
} }
} }

9
youmubot-osu/src/test.rs Normal file

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,9 @@ chrono = "0.4.9"
rand = "0.7.2" rand = "0.7.2"
static_assertions = "1.1.0" static_assertions = "1.1.0"
reqwest = "0.9.24" reqwest = "0.9.24"
regex = "1"
lazy_static = "1"
youmubot-osu = { path = "../youmubot-osu" }
[dependencies.rustbreak] [dependencies.rustbreak]
version = "2.0.0-rc3" version = "2.0.0-rc3"

View file

@ -12,6 +12,7 @@ mod args;
pub mod admin; pub mod admin;
pub mod community; pub mod community;
pub mod fun; pub mod fun;
pub mod osu;
pub use admin::ADMIN_GROUP; pub use admin::ADMIN_GROUP;
pub use community::COMMUNITY_GROUP; pub use community::COMMUNITY_GROUP;

View file

@ -0,0 +1,303 @@
use crate::commands::args::Duration;
use crate::http;
use lazy_static::lazy_static;
use regex::Regex;
use serenity::{
builder::CreateEmbed,
framework::standard::{macros::group, CommandResult},
model::{channel::Message, id::ChannelId},
prelude::*,
utils::MessageBuilder,
};
use youmubot_osu::{
models::{Beatmap, Mode},
request::BeatmapRequestKind,
};
group!({
name: "osu",
options: {
prefix: "osu",
description: "osu! related commands.",
},
commands: [],
});
lazy_static! {
static ref OLD_LINK_REGEX: Regex = Regex::new(
r"https?://osu\.ppy\.sh/(?P<link_type>s|b)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>\d))?(?:\+(?P<mods>[A-Z]+))?"
).unwrap();
static ref NEW_LINK_REGEX: Regex = Regex::new(
r"https?://osu\.ppy\.sh/beatmapsets/(?P<set_id>\d+)/?(?:\#(?P<mode>osu|taiko|fruits|mania)(?:/(?P<beatmap_id>\d+)|/?))?(?:\+(?P<mods>[A-Z]+))?"
).unwrap();
}
pub fn hook(ctx: &mut Context, msg: &Message) -> () {
Some(msg)
.filter(|&m| !m.author.bot) // Don't react to bot messages
.map(|m| {
if let Err(v) = handle_old_links(ctx, &m.content, m.channel_id) {
println!("Error on old link handling: {:?}", v);
}
});
}
fn handle_old_links(ctx: &mut Context, content: impl AsRef<str>, ch: ChannelId) -> CommandResult {
let data = ctx.data.write();
let reqwest = data.get::<http::HTTP>().unwrap();
let osu = data.get::<http::Osu>().unwrap();
for capture in OLD_LINK_REGEX.captures_iter(content.as_ref()) {
let req_type = capture.name("link_type").unwrap().as_str();
let req = match req_type {
"b" => BeatmapRequestKind::Beatmap(capture["id"].parse()?),
"s" => BeatmapRequestKind::Beatmapset(capture["id"].parse()?),
_ => continue,
};
let mode = capture
.name("mode")
.map(|v| v.as_str().parse())
.transpose()?
.and_then(|v| {
Some(match v {
0 => Mode::Std,
1 => Mode::Taiko,
2 => Mode::Catch,
3 => Mode::Mania,
_ => return None,
})
});
let mut beatmaps = osu.beatmaps(reqwest, req, |v| match mode {
Some(m) => v.mode(m, true),
None => v,
})?;
match req_type {
"b" => {
for beatmap in beatmaps.iter() {
if let Err(v) = ch.send_message(&ctx, |m| {
m.content(
MessageBuilder::new()
.push("Beatmap information for ")
.push_mono_safe(&capture[0])
.build(),
)
.embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), b))
}) {
println!("Error on printing beatmap {}: {:?}", beatmap.beatmap_id, v);
}
}
}
"s" => {
beatmaps.sort_by(|a, b| {
(mode.unwrap_or(a.mode) as u8, a.difficulty.stars)
.partial_cmp(&(mode.unwrap_or(b.mode) as u8, b.difficulty.stars))
.unwrap()
});
ch.send_message(&ctx, |m| {
m.content(
MessageBuilder::new()
.push("Beatmapset information for ")
.push_mono_safe(&capture[0])
.build(),
)
.embed(|b| beatmapset_embed(&beatmaps, mode, b))
})?;
}
_ => (),
}
}
Ok(())
}
const NEW_MODE_NAMES: [&'static str; 4] = ["osu", "taiko", "fruits", "mania"];
fn format_mode(actual: Mode, original: Mode) -> String {
if actual == original {
format!("{}", actual)
} else {
format!("{} (converted)", actual)
}
}
fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a mut CreateEmbed {
c.title(
MessageBuilder::new()
.push_bold_safe(&b.artist)
.push(" - ")
.push_bold_safe(&b.title)
.push(" [")
.push_bold_safe(&b.difficulty_name)
.push("]")
.build(),
)
.author(|a| {
a.name(&b.creator)
.url(format!("https://osu.ppy.sh/users/{}", b.creator_id))
.icon_url(format!("https://a.ppy.sh/{}", b.creator_id))
})
.url(format!(
"https://osu.ppy.sh/beatmapsets/{}/#{}/{}",
b.beatmapset_id, NEW_MODE_NAMES[b.mode as usize], b.beatmap_id
))
.thumbnail(format!("https://b.ppy.sh/thumb/{}l.jpg", b.beatmapset_id))
.image(format!(
"https://assets.ppy.sh/beatmaps/{}/covers/cover.jpg",
b.beatmapset_id
))
.color(0xffb6c1)
.field(
"Star Difficulty",
format!("{:.2}", b.difficulty.stars),
false,
)
.field(
"Length",
MessageBuilder::new()
.push_bold_safe(Duration(b.total_length))
.push(" (")
.push_bold_safe(Duration(b.drain_length))
.push(" drain)")
.build(),
false,
)
.field("Circle Size", format!("{:.1}", b.difficulty.cs), true)
.field("Approach Rate", format!("{:.1}", b.difficulty.ar), true)
.field(
"Overall Difficulty",
format!("{:.1}", b.difficulty.od),
true,
)
.field("HP Drain", format!("{:.1}", b.difficulty.hp), true)
.field("BPM", b.bpm.round(), true)
.fields(b.difficulty.max_combo.map(|v| ("Max combo", v, true)))
.field("Mode", format_mode(m, b.mode), true)
.fields(b.source.as_ref().map(|v| ("Source", v, true)))
.field(
"Tags",
b.tags
.iter()
.map(|v| MessageBuilder::new().push_mono_safe(v).build())
.take(10)
.chain(std::iter::once("...".to_owned()))
.collect::<Vec<_>>()
.join(" "),
false,
)
.description(
MessageBuilder::new()
.push_line({
let link = format!("https://osu.ppy.sh/beatmapsets/{}/download", b.beatmap_id);
format!(
"Download: [[Link]]({}) [[No Video]]({}?noVideo=1)",
link, link
)
})
.push_line(format!(
"Beatmapset: https://osu.ppy.sh/beatmapsets/{}/#{}",
b.beatmapset_id, NEW_MODE_NAMES[b.mode as usize],
))
.build(),
)
}
const MAX_DIFFS: usize = 25 - 4;
fn beatmapset_embed<'a>(
bs: &'_ [Beatmap],
m: Option<Mode>,
c: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed {
let too_many_diffs = bs.len() > MAX_DIFFS;
let b: &Beatmap = &bs[0];
c.title(
MessageBuilder::new()
.push_bold_safe(&b.artist)
.push(" - ")
.push_bold_safe(&b.title)
.build(),
)
.author(|a| {
a.name(&b.creator)
.url(format!("https://osu.ppy.sh/users/{}", b.creator_id))
.icon_url(format!("https://a.ppy.sh/{}", b.creator_id))
})
.url(format!(
"https://osu.ppy.sh/beatmapsets/{}",
b.beatmapset_id,
))
// .thumbnail(format!("https://b.ppy.sh/thumb/{}l.jpg", b.beatmapset_id))
.image(format!(
"https://assets.ppy.sh/beatmaps/{}/covers/cover.jpg",
b.beatmapset_id
))
.color(0xffb6c1)
.description(
MessageBuilder::new()
.push_line({
let link = format!("https://osu.ppy.sh/beatmapsets/{}/download", b.beatmap_id);
format!(
"Download: [[Link]]({}) [[No Video]]({}?noVideo=1)",
link, link
)
})
.build(),
)
.field(
"Length",
MessageBuilder::new()
.push_bold_safe(Duration(b.total_length))
.build(),
true,
)
.field("BPM", b.bpm.round(), true)
.fields(b.source.as_ref().map(|v| ("Source", v, false)))
.field(
"Tags",
b.tags
.iter()
.map(|v| MessageBuilder::new().push_mono_safe(v).build())
.take(10)
.chain(std::iter::once("...".to_owned()))
.collect::<Vec<_>>()
.join(" "),
false,
)
.footer(|f| {
if too_many_diffs {
f.text(format!(
"This map has {} diffs, we are showing the first {}.",
bs.len(),
MAX_DIFFS
))
} else {
f
}
})
.fields(bs.iter().take(MAX_DIFFS).map(|b: &Beatmap| {
(
format!("[{}]", b.difficulty_name),
MessageBuilder::new()
.push(format!(
"[[Link]](https://osu.ppy.sh/beatmapsets/{}/#{}/{})",
b.beatmapset_id,
NEW_MODE_NAMES[m.unwrap_or(b.mode) as usize],
b.beatmap_id
))
.push(", ")
.push_bold(format!("{:.2}", b.difficulty.stars))
.push(", ")
.push_bold_line(format_mode(m.unwrap_or(b.mode), b.mode))
.push("CS")
.push_bold(format!("{:.1}", b.difficulty.cs))
.push(", AR")
.push_bold(format!("{:.1}", b.difficulty.ar))
.push(", OD")
.push_bold(format!("{:.1}", b.difficulty.od))
.push(", HP")
.push_bold(format!("{:.1}", b.difficulty.hp))
.push(", ⌛ ")
.push_bold(format!("{}", Duration(b.drain_length)))
.build(),
false,
)
}))
}

View file

@ -1,7 +1,14 @@
use serenity::prelude::TypeMapKey; use serenity::prelude::TypeMapKey;
use youmubot_osu::Client as OsuClient;
pub(crate) struct HTTP; pub(crate) struct HTTP;
impl TypeMapKey for HTTP { impl TypeMapKey for HTTP {
type Value = reqwest::Client; type Value = reqwest::Client;
} }
pub(crate) struct Osu;
impl TypeMapKey for Osu {
type Value = OsuClient;
}

View file

@ -3,20 +3,27 @@ use dotenv::var;
use reqwest; use reqwest;
use serenity::{ use serenity::{
framework::standard::{DispatchError, StandardFramework}, framework::standard::{DispatchError, StandardFramework},
model::gateway, model::{channel::Message, gateway},
prelude::*, prelude::*,
}; };
use youmubot_osu::Client as OsuClient;
mod commands; mod commands;
mod db; mod db;
mod http; mod http;
const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [commands::osu::hook];
struct Handler; struct Handler;
impl EventHandler for Handler { impl EventHandler for Handler {
fn ready(&self, _: Context, ready: gateway::Ready) { fn ready(&self, _: Context, ready: gateway::Ready) {
println!("{} is connected!", ready.user.name); println!("{} is connected!", ready.user.name);
} }
fn message(&self, mut ctx: Context, message: Message) {
MESSAGE_HOOKS.iter().for_each(|f| f(&mut ctx, &message));
}
} }
fn main() { fn main() {
@ -39,6 +46,9 @@ fn main() {
{ {
let mut data = client.data.write(); let mut data = client.data.write();
data.insert::<http::HTTP>(reqwest::Client::new()); data.insert::<http::HTTP>(reqwest::Client::new());
data.insert::<http::Osu>(OsuClient::new(
var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
));
} }
// Create handler threads // Create handler threads