mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-18 16:28:55 +00:00
Implement get_scores requests
This commit is contained in:
parent
71041533bf
commit
3e951554d7
9 changed files with 282 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
target
|
||||
.env
|
||||
*.ron
|
||||
cargo-remote
|
||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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)",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
129
youmubot-osu/src/models/mods.rs
Normal file
129
youmubot-osu/src/models/mods.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Add table
Reference in a new issue