From 3e951554d7dd0e45fe7e3e16d2ea2f8065cc1183 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 28 Dec 2019 10:45:07 +0900 Subject: [PATCH] Implement get_scores requests --- .gitignore | 1 + Cargo.lock | 1 + youmubot-osu/Cargo.toml | 1 + youmubot-osu/src/lib.rs | 12 +++ youmubot-osu/src/models/deser.rs | 28 +++++++ youmubot-osu/src/models/mod.rs | 33 +++++++- youmubot-osu/src/models/mods.rs | 129 +++++++++++++++++++++++++++++++ youmubot-osu/src/models/raw.rs | 21 +++++ youmubot-osu/src/request.rs | 69 ++++++++++++++--- 9 files changed, 282 insertions(+), 13 deletions(-) create mode 100644 youmubot-osu/src/models/mods.rs diff --git a/.gitignore b/.gitignore index 3ca3667..9bdf0ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target .env *.ron +cargo-remote diff --git a/Cargo.lock b/Cargo.lock index 8e32c62..e5a3bee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1836,6 +1836,7 @@ dependencies = [ name = "youmubot-osu" version = "0.1.0" dependencies = [ + "bitflags 1.2.1 (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)", "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index 99f873e..5c7f07c 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -11,6 +11,7 @@ serenity = "0.7" chrono = "0.4.10" reqwest = "0.9.24" serde = { version = "1.0", features = ["derive"] } +bitflags = "1" [dev-dependencies] serde_json = "1" diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index 47420f7..68548fb 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -47,4 +47,16 @@ impl Client { let res: Vec<_> = r.build(client).query(&[("k", &self.key)]).send()?.json()?; Ok(res.into_iter().next()) } + + pub fn scores( + &self, + client: &HTTPClient, + beatmap_id: u64, + f: impl FnOnce(&mut ScoreRequestBuilder) -> &mut ScoreRequestBuilder, + ) -> Result, Error> { + let mut r = ScoreRequestBuilder::new(beatmap_id); + 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 d87acca..345180f 100644 --- a/youmubot-osu/src/models/deser.rs +++ b/youmubot-osu/src/models/deser.rs @@ -6,6 +6,34 @@ use chrono::{ use serde::{de, Deserialize, Deserializer}; use std::str::FromStr; +impl<'de> Deserialize<'de> for Score { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw: raw::Score = raw::Score::deserialize(deserializer)?; + Ok(Score { + id: parse_from_str(&raw.score_id)?, + username: raw.username, + user_id: parse_from_str(&raw.user_id)?, + date: parse_date(&raw.date)?, + replay_available: parse_bool(&raw.replay_available)?, + score: parse_from_str(&raw.score)?, + pp: parse_from_str(&raw.pp)?, + rank: parse_from_str(&raw.rank)?, + mods: parse_from_str(&raw.enabled_mods)?, + count_300: parse_from_str(&raw.count300)?, + count_100: parse_from_str(&raw.count100)?, + count_50: parse_from_str(&raw.count50)?, + count_miss: parse_from_str(&raw.countmiss)?, + count_katu: parse_from_str(&raw.countkatu)?, + count_geki: parse_from_str(&raw.countgeki)?, + max_combo: parse_from_str(&raw.maxcombo)?, + perfect: parse_bool(&raw.perfect)?, + }) + } +} + impl<'de> Deserialize<'de> for User { fn deserialize(deserializer: D) -> Result where diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 645b96b..f53d225 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -2,8 +2,11 @@ use chrono::{DateTime, Duration, Utc}; use std::fmt; pub mod deser; +pub mod mods; pub(crate) mod raw; +pub use mods::Mods; + #[derive(Debug)] pub enum ApprovalStatus { Loved, @@ -146,6 +149,7 @@ pub struct Beatmap { pub pass_count: u64, } +#[derive(Debug)] pub struct UserEvent { pub display_html: String, pub beatmap_id: u64, @@ -154,6 +158,7 @@ pub struct UserEvent { pub epic_factor: u8, } +#[derive(Debug)] pub struct User { pub id: u64, pub username: String, @@ -181,6 +186,7 @@ pub struct User { pub accuracy: f64, } +#[derive(Debug)] pub enum Rank { SS, SSH, @@ -193,6 +199,31 @@ pub enum Rank { F, } +impl std::str::FromStr for Rank { + type Err = String; + fn from_str(a: &str) -> Result { + Ok(match &a.to_uppercase()[..] { + "SS" => Rank::SS, + "SSH" => Rank::SSH, + "S" => Rank::S, + "SH" => Rank::SH, + "A" => Rank::A, + "B" => Rank::B, + "C" => Rank::C, + "D" => Rank::D, + "F" => Rank::F, + t => return Err(format!("Invalid value {}", t)), + }) + } +} + +impl fmt::Display for Rank { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug)] pub struct Score { pub id: u64, pub username: String, @@ -203,7 +234,7 @@ pub struct Score { pub score: u64, pub pp: f64, pub rank: Rank, - pub mods: u64, // Later + pub mods: Mods, // Later pub count_300: u64, pub count_100: u64, diff --git a/youmubot-osu/src/models/mods.rs b/youmubot-osu/src/models/mods.rs new file mode 100644 index 0000000..43b92f4 --- /dev/null +++ b/youmubot-osu/src/models/mods.rs @@ -0,0 +1,129 @@ +use std::fmt; + +bitflags::bitflags! { + /// The mods available to osu! + #[derive(std::default::Default)] + pub struct Mods: u64 { + const NOMOD = 0; + const NF = 1 << 0; + const EZ = 1 << 1; + const TD = 1 << 2; + const HD = 1 << 3; + const HR = 1 << 4; + const SD = 1 << 5; + const DT = 1 << 6; + const RX = 1 << 7; + const HT = 1 << 8; + const NC = 1 << 9; + const FL = 1 << 10; + const AT = 1 << 11; + const SO = 1 << 12; + const AP = 1 << 13; + const PF = 1 << 14; + const KEY4 = 1 << 15; /* TODO: what are these abbreviated to? */ + const KEY5 = 1 << 16; + const KEY6 = 1 << 17; + const KEY7 = 1 << 18; + const KEY8 = 1 << 19; + const FADEIN = 1 << 20; + const RANDOM = 1 << 21; + const CINEMA = 1 << 22; + const TARGET = 1 << 23; + const KEY9 = 1 << 24; + const KEYCOOP = 1 << 25; + const KEY1 = 1 << 26; + const KEY3 = 1 << 27; + const KEY2 = 1 << 28; + const SCOREV2 = 1 << 29; + const TOUCH_DEVICE = Self::TD.bits; + const NOVIDEO = Self::TD.bits; /* never forget */ + const SPEED_CHANGING = Self::DT.bits | Self::HT.bits | Self::NC.bits; + const MAP_CHANGING = Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits; + } +} + +const MODS_WITH_NAMES: &[(Mods, &'static str)] = &[ + (Mods::NF, "NF"), + (Mods::EZ, "EZ"), + (Mods::TD, "TD"), + (Mods::HD, "HD"), + (Mods::HR, "HR"), + (Mods::SD, "SD"), + (Mods::DT, "DT"), + (Mods::RX, "RX"), + (Mods::HT, "HT"), + (Mods::NC, "NC"), + (Mods::FL, "FL"), + (Mods::AT, "AT"), + (Mods::SO, "SO"), + (Mods::AP, "AP"), + (Mods::PF, "PF"), + (Mods::KEY1, "1K"), + (Mods::KEY2, "2K"), + (Mods::KEY3, "3K"), + (Mods::KEY4, "4K"), + (Mods::KEY5, "5K"), + (Mods::KEY6, "6K"), + (Mods::KEY7, "7K"), + (Mods::KEY8, "8K"), + (Mods::KEY9, "9K"), +]; + +impl std::str::FromStr for Mods { + type Err = String; + fn from_str(mut s: &str) -> Result { + let mut res = Self::default(); + while s.len() >= 2 { + let (m, nw) = s.split_at(2); + s = nw; + match &m.to_uppercase()[..] { + "NF" => res |= Mods::NF, + "EZ" => res |= Mods::EZ, + "TD" => res |= Mods::TD, + "HD" => res |= Mods::HD, + "HR" => res |= Mods::HR, + "SD" => res |= Mods::SD, + "DT" => res |= Mods::DT, + "RX" => res |= Mods::RX, + "HT" => res |= Mods::HT, + "NC" => res |= Mods::NC, + "FL" => res |= Mods::FL, + "AT" => res |= Mods::AT, + "SO" => res |= Mods::SO, + "AP" => res |= Mods::AP, + "PF" => res |= Mods::PF, + "1K" => res |= Mods::KEY1, + "2K" => res |= Mods::KEY2, + "3K" => res |= Mods::KEY3, + "4K" => res |= Mods::KEY4, + "5K" => res |= Mods::KEY5, + "6K" => res |= Mods::KEY6, + "7K" => res |= Mods::KEY7, + "8K" => res |= Mods::KEY8, + "9K" => res |= Mods::KEY9, + v => return Err(format!("{} is not a valid mod", v)), + } + } + if s.len() > 0 { + Err("String of odd length is not a mod string".to_owned()) + } else { + Ok(res) + } + } +} + +impl fmt::Display for Mods { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_empty() { + // Return an empty string + return Ok(()); + } + write!(f, "+")?; + for p in MODS_WITH_NAMES.iter() { + if self.contains(p.0) { + write!(f, "{}", p.1)?; + } + } + Ok(()) + } +} diff --git a/youmubot-osu/src/models/raw.rs b/youmubot-osu/src/models/raw.rs index 39242e0..ba70742 100644 --- a/youmubot-osu/src/models/raw.rs +++ b/youmubot-osu/src/models/raw.rs @@ -75,3 +75,24 @@ pub(crate) struct UserEvent { pub date: String, pub epicfactor: String, } + +#[derive(Deserialize, Debug)] +pub(crate) struct Score { + pub score_id: String, + pub score: String, + pub username: String, + pub count300: String, + pub count100: String, + pub count50: String, + pub countmiss: String, + pub maxcombo: String, + pub countkatu: String, + pub countgeki: String, + pub perfect: String, + pub enabled_mods: String, + pub user_id: String, + pub date: String, + pub rank: String, + pub pp: String, + pub replay_available: String, +} diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index 4b7be84..eac31be 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -1,4 +1,4 @@ -use crate::models::Mode; +use crate::models::{Mode, Mods}; use chrono::{DateTime, Utc}; use reqwest::{Client, RequestBuilder}; @@ -15,6 +15,12 @@ impl ToQuery for Option { } } +impl ToQuery for Mods { + fn to_query(&self) -> Vec<(&'static str, String)> { + vec![("mods", format!("{}", self.bits()))] + } +} + impl ToQuery for Mode { fn to_query(&self) -> Vec<(&'static str, String)> { vec![("m", (*self as u8).to_string())] @@ -151,19 +157,58 @@ pub mod builders { ) } } + + pub struct ScoreRequestBuilder { + beatmap_id: u64, + user: Option, + mode: Option, + mods: Option, + limit: Option, + } + + impl ScoreRequestBuilder { + pub(crate) fn new(beatmap_id: u64) -> Self { + ScoreRequestBuilder { + beatmap_id, + user: None, + mode: None, + mods: None, + limit: None, + } + } + + pub fn user(&mut self, u: UserID) -> &mut Self { + self.user = Some(u); + self + } + + pub fn mode(&mut self, mode: Mode) -> &mut Self { + self.mode = Some(mode); + self + } + + pub fn mods(&mut self, mods: Mods) -> &mut Self { + self.mods = Some(mods); + self + } + + pub fn limit(&mut self, limit: u8) -> &mut Self { + self.limit = Some(limit).filter(|&v| v <= 100).or(self.limit); + self + } + + pub(crate) fn build(&self, client: &Client) -> RequestBuilder { + client + .get("https://osu.ppy.sh/api/get_scores") + .query(&[("b", self.beatmap_id)]) + .query(&self.user.to_query()) + .query(&self.mode.to_query()) + .query(&self.mods.to_query()) + .query(&self.limit.map(|v| ("limit", v.to_string())).to_query()) + } + } } -pub struct UserRequest { - pub user: UserID, - pub mode: Option, - pub event_days: Option, -} -pub struct ScoreRequest { - pub beatmap_id: u64, - pub user: Option, - pub mode: Option, - pub mods: u64, // Later -} pub struct UserBestRequest { pub user: UserID, pub mode: Option,