Implement get_scores requests

This commit is contained in:
Natsu Kagami 2019-12-28 10:45:07 +09:00
parent 71041533bf
commit 3e951554d7
9 changed files with 282 additions and 13 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
target
.env
*.ron
cargo-remote

1
Cargo.lock generated
View file

@ -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)",

View file

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

View file

@ -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<Vec<Score>, Error> {
let mut r = ScoreRequestBuilder::new(beatmap_id);
f(&mut r);
let res = r.build(client).query(&[("k", &self.key)]).send()?.json()?;
Ok(res)
}
}

View file

@ -6,6 +6,34 @@ use chrono::{
use serde::{de, Deserialize, Deserializer};
use std::str::FromStr;
impl<'de> Deserialize<'de> for Score {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
where

View file

@ -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<Self, Self::Err> {
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,

View file

@ -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<Self, Self::Err> {
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(())
}
}

View file

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

View file

@ -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<T: ToQuery> ToQuery for Option<T> {
}
}
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<UserID>,
mode: Option<Mode>,
mods: Option<Mods>,
limit: Option<u8>,
}
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<Mode>,
pub event_days: Option<u8>,
}
pub struct ScoreRequest {
pub beatmap_id: u64,
pub user: Option<UserID>,
pub mode: Option<Mode>,
pub mods: u64, // Later
}
pub struct UserBestRequest {
pub user: UserID,
pub mode: Option<Mode>,