diff --git a/Cargo.lock b/Cargo.lock index 8707751..8e32c62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,6 +5,14 @@ name = "adler32" version = "1.0.4" 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]] name = "anyhow" version = "1.0.25" @@ -1000,7 +1008,10 @@ name = "regex" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1323,6 +1334,14 @@ dependencies = [ "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]] name = "threadpool" version = "1.7.1" @@ -1802,12 +1821,15 @@ version = "0.1.0" dependencies = [ "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)", + "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)", + "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)", "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)", "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)", + "youmubot-osu 0.1.0", ] [[package]] @@ -1817,11 +1839,13 @@ dependencies = [ "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)", "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)", ] [metadata] "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 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" @@ -1972,6 +1996,7 @@ dependencies = [ "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 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 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" diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index 77e1b7e..99f873e 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -11,3 +11,6 @@ serenity = "0.7" chrono = "0.4.10" reqwest = "0.9.24" serde = { version = "1.0", features = ["derive"] } + +[dev-dependencies] +serde_json = "1" diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index 7213555..089dbce 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -2,6 +2,9 @@ pub mod models; pub mod request; +#[cfg(test)] +mod test; + use models::*; use request::builders::*; use request::*; @@ -25,13 +28,11 @@ impl Client { &self, client: &HTTPClient, kind: BeatmapRequestKind, - f: impl FnOnce(BeatmapRequestBuilder) -> BeatmapRequestBuilder, + f: impl FnOnce(&mut BeatmapRequestBuilder) -> &mut BeatmapRequestBuilder, ) -> Result, Error> { - let res = f(BeatmapRequestBuilder::new(kind)) - .build(client) - .query(&[("k", &self.key)]) - .send()? - .json()?; + let mut r = BeatmapRequestBuilder::new(kind); + f(&mut r); + let res = r.build(client).query(&[("k", &self.key)]).send()?.json()?; Ok(res) } } diff --git a/youmubot-osu/src/models/deser.rs b/youmubot-osu/src/models/deser.rs index 8c5adeb..0e7381a 100644 --- a/youmubot-osu/src/models/deser.rs +++ b/youmubot-osu/src/models/deser.rs @@ -25,15 +25,15 @@ impl<'de> Deserialize<'de> for Beatmap { bpm: parse_from_str(&raw.bpm)?, creator: raw.creator, 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)?, language: parse_language(&raw.language_id)?, tags: raw.tags.split_whitespace().map(|v| v.to_owned()).collect(), difficulty_name: raw.version, difficulty: Difficulty { stars: parse_from_str(&raw.difficultyrating)?, - aim: parse_from_str(&raw.diff_aim)?, - speed: parse_from_str(&raw.diff_speed)?, + aim: raw.diff_aim.map(parse_from_str).transpose()?, + speed: raw.diff_speed.map(parse_from_str).transpose()?, cs: parse_from_str(&raw.diff_size)?, od: parse_from_str(&raw.diff_overall)?, 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_slider: parse_from_str(&raw.count_slider)?, 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)?, total_length: parse_duration(&raw.total_length)?, @@ -61,8 +61,8 @@ fn parse_mode(s: impl AsRef) -> Result { Ok(match t { 0 => Std, 1 => Taiko, - 2 => Mania, - 3 => Catch, + 2 => Catch, + 3 => Mania, _ => return Err(E::custom(format!("invalid value {} for mode", t))), }) } @@ -83,7 +83,7 @@ fn parse_language(s: impl AsRef) -> Result { 9 => Swedish, 10 => Spanish, 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(s: impl AsRef) -> Result { 7 => Novelty, 9 => HipHop, 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(s: impl AsRef) -> Result { } fn parse_from_str(s: impl AsRef) -> Result { - 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(b: impl AsRef) -> Result { match b.as_ref() { "1" => Ok(true), "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(b: &raw::Beatmap) -> Result Loved, "3" => Qualified, "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, "-1" => WIP, "-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(date: impl AsRef) -> Result, E> { date.as_ref(), (&[ Item::Numeric(Numeric::Year, Pad::Zero), - Item::Literal("/"), + Item::Literal("-"), Item::Numeric(Numeric::Month, Pad::Zero), - Item::Literal("/"), + Item::Literal("-"), Item::Numeric(Numeric::Day, Pad::Zero), Item::Space(""), Item::Numeric(Numeric::Hour, Pad::Zero), - Item::Literal("-"), + Item::Literal(":"), Item::Numeric(Numeric::Minute, Pad::Zero), - Item::Literal("-"), + Item::Literal(":"), Item::Numeric(Numeric::Second, Pad::Zero), ]) .iter(), diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 38afff4..00a829e 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -18,8 +18,8 @@ pub enum ApprovalStatus { #[derive(Debug)] pub struct Difficulty { pub stars: f64, - pub aim: f64, - pub speed: f64, + pub aim: Option, + pub speed: Option, pub cs: f64, pub od: f64, @@ -29,7 +29,7 @@ pub struct Difficulty { pub count_normal: u64, pub count_slider: u64, pub count_spinner: u64, - pub max_combo: u64, + pub max_combo: Option, } #[derive(Debug)] @@ -61,17 +61,27 @@ pub enum Language { Spanish, Italian, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mode { Std, Taiko, - Mania, Catch, + Mania, } -impl ToString for Mode { - fn to_string(&self) -> String { - (*self as u64).to_string() +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Mode::*; + write!( + f, + "{}", + match self { + Std => "osu!", + Taiko => "osu!taiko", + Mania => "osu!mania", + Catch => "osu!catch", + } + ) } } diff --git a/youmubot-osu/src/models/raw.rs b/youmubot-osu/src/models/raw.rs index d1056ca..7b80308 100644 --- a/youmubot-osu/src/models/raw.rs +++ b/youmubot-osu/src/models/raw.rs @@ -1,10 +1,10 @@ use serde::Deserialize; -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub(crate) struct Beatmap { pub approved: String, pub submit_date: String, - pub approved_date: String, + pub approved_date: Option, pub last_update: String, pub artist: String, pub beatmap_id: String, @@ -13,8 +13,8 @@ pub(crate) struct Beatmap { pub creator: String, pub creator_id: String, pub difficultyrating: String, - pub diff_aim: String, - pub diff_speed: String, + pub diff_aim: Option, + pub diff_speed: Option, pub diff_size: String, pub diff_overall: String, pub diff_approach: String, @@ -36,7 +36,7 @@ pub(crate) struct Beatmap { pub count_normal: String, pub count_slider: String, pub count_spinner: String, - pub max_combo: String, + pub max_combo: Option, pub download_unavailable: String, pub audio_unavailable: String, } diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index 515b7eb..a17415a 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -15,9 +15,12 @@ impl ToQuery for Option { } } -impl ToQuery for Mode { +impl ToQuery for (Mode, bool) { 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 { kind: BeatmapRequestKind, since: Option>, - mode: Mode, - converted: bool, + mode: Option<(Mode, /* Converted */ bool)>, } impl BeatmapRequestBuilder { pub(crate) fn new(kind: BeatmapRequestKind) -> Self { BeatmapRequestBuilder { kind, since: None, - mode: Mode::Std, - converted: false, + mode: None, } } - pub fn since(&mut self, since: DateTime) -> &Self { + pub fn since(&mut self, since: DateTime) -> &mut Self { self.since = Some(since); self } - pub fn mode(&mut self, mode: Mode, converted: bool) -> &Self { - self.mode = mode; - self.converted = converted; + pub fn mode(&mut self, mode: Mode, converted: bool) -> &mut Self { + self.mode = Some((mode, converted)); self } @@ -104,14 +104,6 @@ pub mod builders { .query(&self.kind.to_query()) .query(&self.since.map(|v| ("since", v)).to_query()) .query(&self.mode.to_query()) - .query( - &(if self.converted { - Some(("a", "1".to_owned())) - } else { - None - }) - .to_query(), - ) } } } diff --git a/youmubot-osu/src/test.rs b/youmubot-osu/src/test.rs new file mode 100644 index 0000000..63aea3e --- /dev/null +++ b/youmubot-osu/src/test.rs @@ -0,0 +1,9 @@ +const INPUT: &str = r#"[{"beatmapset_id":"542081","beatmap_id":"1149303","approved":"1","total_length":"44","hit_length":"42","version":"Insane","file_md5":"65bbda0ff3f3a8aa2add745d2ad90209","diff_size":"4.1","diff_overall":"8.2","diff_approach":"8","diff_drain":"5.5","mode":"0","count_normal":"117","count_slider":"24","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"1850215","passcount":"241349","max_combo":"172","diff_aim":"2.28424","diff_speed":"1.72122","difficultyrating":"4.28697"},{"beatmapset_id":"542081","beatmap_id":"1149619","approved":"1","total_length":"44","hit_length":"39","version":"Rain's Easy","file_md5":"49ee0a0d3a75fa95dc6cb655a99c9fcd","diff_size":"3","diff_overall":"2","diff_approach":"3","diff_drain":"2","mode":"0","count_normal":"18","count_slider":"22","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"277708","passcount":"168926","max_combo":"72","diff_aim":"0.802138","diff_speed":"0.706338","difficultyrating":"1.55638"},{"beatmapset_id":"542081","beatmap_id":"1149638","approved":"1","total_length":"44","hit_length":"42","version":"Sotarks' Insane","file_md5":"033dc9f25e5db2cb0296465b18b9b424","diff_size":"4","diff_overall":"8","diff_approach":"8","diff_drain":"5.5","mode":"0","count_normal":"82","count_slider":"38","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"2431913","passcount":"371089","max_combo":"160","diff_aim":"2.28972","diff_speed":"1.62158","difficultyrating":"4.24537"},{"beatmapset_id":"542081","beatmap_id":"1149713","approved":"1","total_length":"44","hit_length":"42","version":"Linada's Insane","file_md5":"0b6c067c2370af9d42fc9d49dd4408d5","diff_size":"3.6","diff_overall":"7.5","diff_approach":"8","diff_drain":"5.5","mode":"0","count_normal":"87","count_slider":"37","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"1993990","passcount":"372872","max_combo":"163","diff_aim":"2.05241","diff_speed":"1.61859","difficultyrating":"3.88792"},{"beatmapset_id":"542081","beatmap_id":"1149864","approved":"1","total_length":"44","hit_length":"42","version":"Akitoshi's Hard","file_md5":"cf873fddb4335c1b20cb7670e9efe366","diff_size":"3.5","diff_overall":"6","diff_approach":"7","diff_drain":"5","mode":"0","count_normal":"68","count_slider":"40","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"721693","passcount":"213582","max_combo":"150","diff_aim":"1.49837","diff_speed":"1.41785","difficultyrating":"2.95649"},{"beatmapset_id":"542081","beatmap_id":"1149925","approved":"1","total_length":"44","hit_length":"42","version":"Foxy's Hard","file_md5":"ef54e15e1f13ff881503574abcb8d277","diff_size":"3.5","diff_overall":"6.5","diff_approach":"7.5","diff_drain":"5","mode":"0","count_normal":"86","count_slider":"38","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"1415732","passcount":"270135","max_combo":"164","diff_aim":"1.88298","diff_speed":"1.6238","difficultyrating":"3.63637"},{"beatmapset_id":"542081","beatmap_id":"1150597","approved":"1","total_length":"41","hit_length":"39","version":"NiNo's Insane","file_md5":"395faa3a0c0dc3daf93a016e9e14ef9d","diff_size":"4.5","diff_overall":"8.3","diff_approach":"8","diff_drain":"5.5","mode":"0","count_normal":"119","count_slider":"21","count_spinner":"1","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"1036842","passcount":"107883","max_combo":"162","diff_aim":"2.45971","diff_speed":"1.73208","difficultyrating":"4.55561"},{"beatmapset_id":"542081","beatmap_id":"1153219","approved":"1","total_length":"44","hit_length":"42","version":"Haru's Insane","file_md5":"631a384255f9488ea7af2d47602ebd5e","diff_size":"3.7","diff_overall":"8.2","diff_approach":"8","diff_drain":"5.5","mode":"0","count_normal":"84","count_slider":"37","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"1301255","passcount":"222942","max_combo":"160","diff_aim":"2.35354","diff_speed":"1.63119","difficultyrating":"4.34591"},{"beatmapset_id":"542081","beatmap_id":"1153237","approved":"1","total_length":"44","hit_length":"42","version":"Hard","file_md5":"6aaee0420478208725529390e80c0037","diff_size":"4","diff_overall":"7","diff_approach":"7.7","diff_drain":"5","mode":"0","count_normal":"83","count_slider":"37","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"1356021","passcount":"277839","max_combo":"165","diff_aim":"1.91459","diff_speed":"1.67391","difficultyrating":"3.70884"},{"beatmapset_id":"542081","beatmap_id":"1153238","approved":"1","total_length":"44","hit_length":"42","version":"Ren's Insane","file_md5":"80f7447a70261ed53c9da5f2cc97e979","diff_size":"4","diff_overall":"7.3","diff_approach":"7.8","diff_drain":"5","mode":"0","count_normal":"83","count_slider":"39","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"1191468","passcount":"239132","max_combo":"163","diff_aim":"2.07017","diff_speed":"1.60208","difficultyrating":"3.90629"},{"beatmapset_id":"542081","beatmap_id":"1153285","approved":"1","total_length":"44","hit_length":"42","version":"Deppy's Normal","file_md5":"52af38d40492f2b2b2d0f7ede64279f7","diff_size":"3.3","diff_overall":"4","diff_approach":"4.5","diff_drain":"3","mode":"0","count_normal":"23","count_slider":"31","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"251532","passcount":"125554","max_combo":"102","diff_aim":"1.0051","diff_speed":"0.861233","difficultyrating":"1.93826"},{"beatmapset_id":"542081","beatmap_id":"1154509","approved":"1","total_length":"44","hit_length":"42","version":"Irre's Extra","file_md5":"1c268f17386e897bab4171bc7c4e5636","diff_size":"7","diff_overall":"8.6","diff_approach":"8.2","diff_drain":"5","mode":"0","count_normal":"84","count_slider":"32","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"302232","passcount":"52319","max_combo":"152","diff_aim":"2.79578","diff_speed":"1.59379","difficultyrating":"4.99056"},{"beatmapset_id":"542081","beatmap_id":"1155987","approved":"1","total_length":"44","hit_length":"42","version":"Gero's Hard","file_md5":"2c410c85e9637521e2c11a0e77bb4707","diff_size":"5","diff_overall":"7","diff_approach":"7.6","diff_drain":"5","mode":"0","count_normal":"79","count_slider":"41","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"653755","passcount":"141525","max_combo":"185","diff_aim":"1.90333","diff_speed":"1.65433","difficultyrating":"3.68217"},{"beatmapset_id":"542081","beatmap_id":"1158424","approved":"1","total_length":"44","hit_length":"42","version":"Andrea's Hard","file_md5":"2c3e659d9135233a6b71b1a996c01b5d","diff_size":"3.5","diff_overall":"5","diff_approach":"7","diff_drain":"4","mode":"0","count_normal":"53","count_slider":"36","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"473130","passcount":"179583","max_combo":"129","diff_aim":"1.51634","diff_speed":"1.31463","difficultyrating":"2.93182"},{"beatmapset_id":"542081","beatmap_id":"1205930","approved":"1","total_length":"41","hit_length":"39","version":"Ozzy's Oni","file_md5":"1ebd8a29b6fa1d7c6dc7a36cde084a10","diff_size":"5","diff_overall":"5","diff_approach":"5","diff_drain":"5","mode":"1","count_normal":"140","count_slider":"0","count_spinner":"1","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"24915","passcount":"7904","max_combo":null,"diff_aim":null,"diff_speed":null,"difficultyrating":"2.61693"},{"beatmapset_id":"542081","beatmap_id":"1205931","approved":"1","total_length":"41","hit_length":"39","version":"Ozzy's Muzukashii","file_md5":"daf1647f166d2bab8fb7d3bd61ffda61","diff_size":"5","diff_overall":"5","diff_approach":"5","diff_drain":"5","mode":"1","count_normal":"111","count_slider":"0","count_spinner":"1","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"45278","passcount":"19071","max_combo":null,"diff_aim":null,"diff_speed":null,"difficultyrating":"2.20401"},{"beatmapset_id":"542081","beatmap_id":"1207082","approved":"1","total_length":"41","hit_length":"39","version":"Ascendance's Rain","file_md5":"b522a0881628a75a18dfdc1dee94111d","diff_size":"3","diff_overall":"9","diff_approach":"9","diff_drain":"6","mode":"2","count_normal":"37","count_slider":"60","count_spinner":"1","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"69044","passcount":"12022","max_combo":"186","diff_aim":"3.33674","diff_speed":null,"difficultyrating":"3.33674"},{"beatmapset_id":"542081","beatmap_id":"1218649","approved":"1","total_length":"44","hit_length":"42","version":"Extra","file_md5":"9fe0cc30f3f2e8bf29d82ae53eda4520","diff_size":"5.1","diff_overall":"8.5","diff_approach":"8.1","diff_drain":"6","mode":"0","count_normal":"114","count_slider":"26","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"534184","passcount":"60346","max_combo":"172","diff_aim":"2.51862","diff_speed":"1.72511","difficultyrating":"4.64048"},{"beatmapset_id":"542081","beatmap_id":"1373950","approved":"1","total_length":"44","hit_length":"42","version":"Cookie-Triangle-Spicy-Cookie","file_md5":"c45c1b96ca051e721937b0115b1ea0b1","diff_size":"4","diff_overall":"9","diff_approach":"8.3","diff_drain":"6","mode":"0","count_normal":"119","count_slider":"23","count_spinner":"2","submit_date":"2016-12-06 08:06:14","approved_date":"2017-08-14 16:46:34","last_update":"2017-08-07 16:02:51","artist":"Elmo and Cookie Monster","title":"Cookie-Butter-Choco-Cookie","creator":"Monstrata","creator_id":"2706438","bpm":"136","source":"\u30bb\u30b5\u30df\u30b9\u30c8\u30ea\u30fc\u30c8","tags":"cbcc ppap pikotaro sesame street ozzyozrock ozzy irre irreversible nino snownino_ haruto haruto_aizawa haruto- sotarks linada red derandom otaku derandom_otaku gero foxy foxygrandpa akitoshi corinn andrea osuplayer111 deppyforce -_rain_- rain ascendance","genre_id":"7","language_id":"2","favourite_count":"1266","rating":"7.67281","download_unavailable":"0","audio_unavailable":"0","playcount":"2801222","passcount":"290627","max_combo":"172","diff_aim":"2.63586","diff_speed":"1.74142","difficultyrating":"4.8245"}]"#; + +use crate::models::Beatmap; +use serde_json::from_str as parse_str; + +#[test] +fn parse_beatmaps() { + parse_str::>(INPUT).unwrap(); +} diff --git a/youmubot/Cargo.toml b/youmubot/Cargo.toml index 69b46f0..a6eb9a0 100644 --- a/youmubot/Cargo.toml +++ b/youmubot/Cargo.toml @@ -14,6 +14,9 @@ chrono = "0.4.9" rand = "0.7.2" static_assertions = "1.1.0" reqwest = "0.9.24" +regex = "1" +lazy_static = "1" +youmubot-osu = { path = "../youmubot-osu" } [dependencies.rustbreak] version = "2.0.0-rc3" diff --git a/youmubot/src/commands/mod.rs b/youmubot/src/commands/mod.rs index b323cbb..b5faeb9 100644 --- a/youmubot/src/commands/mod.rs +++ b/youmubot/src/commands/mod.rs @@ -12,6 +12,7 @@ mod args; pub mod admin; pub mod community; pub mod fun; +pub mod osu; pub use admin::ADMIN_GROUP; pub use community::COMMUNITY_GROUP; diff --git a/youmubot/src/commands/osu/mod.rs b/youmubot/src/commands/osu/mod.rs new file mode 100644 index 0000000..23425f5 --- /dev/null +++ b/youmubot/src/commands/osu/mod.rs @@ -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/(?Ps|b)/(?P\d+)(?:[\&\?]m=(?P\d))?(?:\+(?P[A-Z]+))?" + ).unwrap(); + static ref NEW_LINK_REGEX: Regex = Regex::new( + r"https?://osu\.ppy\.sh/beatmapsets/(?P\d+)/?(?:\#(?Posu|taiko|fruits|mania)(?:/(?P\d+)|/?))?(?:\+(?P[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, ch: ChannelId) -> CommandResult { + let data = ctx.data.write(); + let reqwest = data.get::().unwrap(); + let osu = data.get::().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::>() + .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, + 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::>() + .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, + ) + })) +} diff --git a/youmubot/src/http.rs b/youmubot/src/http.rs index b065ab5..7633187 100644 --- a/youmubot/src/http.rs +++ b/youmubot/src/http.rs @@ -1,7 +1,14 @@ use serenity::prelude::TypeMapKey; +use youmubot_osu::Client as OsuClient; pub(crate) struct HTTP; impl TypeMapKey for HTTP { type Value = reqwest::Client; } + +pub(crate) struct Osu; + +impl TypeMapKey for Osu { + type Value = OsuClient; +} diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index b122331..e19d343 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -3,20 +3,27 @@ use dotenv::var; use reqwest; use serenity::{ framework::standard::{DispatchError, StandardFramework}, - model::gateway, + model::{channel::Message, gateway}, prelude::*, }; +use youmubot_osu::Client as OsuClient; mod commands; mod db; mod http; +const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [commands::osu::hook]; + struct Handler; impl EventHandler for Handler { fn ready(&self, _: Context, ready: gateway::Ready) { 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() { @@ -39,6 +46,9 @@ fn main() { { let mut data = client.data.write(); data.insert::(reqwest::Client::new()); + data.insert::(OsuClient::new( + var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."), + )); } // Create handler threads