mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 08:48:54 +00:00
Implement score fetching
This commit is contained in:
parent
f5cc201f1c
commit
84b13dcef3
8 changed files with 192 additions and 58 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -899,20 +899,6 @@ dependencies = [
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hyper-rustls"
|
|
||||||
version = "0.23.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
|
|
||||||
dependencies = [
|
|
||||||
"http",
|
|
||||||
"hyper",
|
|
||||||
"rustls 0.20.9",
|
|
||||||
"rustls-native-certs",
|
|
||||||
"tokio",
|
|
||||||
"tokio-rustls 0.23.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.24.2"
|
version = "0.24.2"
|
||||||
|
@ -923,6 +909,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
"rustls 0.21.10",
|
"rustls 0.21.10",
|
||||||
|
"rustls-native-certs",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.24.1",
|
"tokio-rustls 0.24.1",
|
||||||
]
|
]
|
||||||
|
@ -1600,7 +1587,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls 0.24.2",
|
"hyper-rustls",
|
||||||
"hyper-tls",
|
"hyper-tls",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
@ -1670,20 +1657,22 @@ checksum = "be9e281b71d3797817a1e6615dd8fb081dd61359b4c41d08792cc7c3c1c13b4e"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rosu-v2"
|
name = "rosu-v2"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/natsukagami/rosu-v2?rev=6f6731cb2f0d235b006ab375dd94b446dde894ac#6f6731cb2f0d235b006ab375dd94b446dde894ac"
|
||||||
checksum = "ef969d8cb87f8dab58193bd722c7831fc839c9852c6e2aec64da3d2e87d447f3"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"futures",
|
"futures",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls 0.23.2",
|
"hyper-rustls",
|
||||||
|
"itoa",
|
||||||
"leaky-bucket-lite",
|
"leaky-bucket-lite",
|
||||||
"log",
|
"log",
|
||||||
|
"paste",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"smallstr",
|
"smallstr",
|
||||||
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
|
@ -2010,9 +1999,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallstr"
|
name = "smallstr"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f"
|
checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
|
|
@ -16,7 +16,7 @@ osuparse = { git = "https://github.com/eltrufas/osuparse", rev = "ad8f6e5e7771e7
|
||||||
regex = "1.5.6"
|
regex = "1.5.6"
|
||||||
reqwest = "0.11.10"
|
reqwest = "0.11.10"
|
||||||
rosu-pp = "0.9.1"
|
rosu-pp = "0.9.1"
|
||||||
rosu-v2 = "0.8"
|
rosu-v2 = { git = "https://github.com/natsukagami/rosu-v2", rev = "6f6731cb2f0d235b006ab375dd94b446dde894ac" }
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
serde = { version = "1.0.137", features = ["derive"] }
|
serde = { version = "1.0.137", features = ["derive"] }
|
||||||
serenity = "0.11.2"
|
serenity = "0.11.2"
|
||||||
|
|
|
@ -90,14 +90,7 @@ impl Client {
|
||||||
) -> Result<Vec<Score>, Error> {
|
) -> Result<Vec<Score>, Error> {
|
||||||
let mut r = ScoreRequestBuilder::new(beatmap_id);
|
let mut r = ScoreRequestBuilder::new(beatmap_id);
|
||||||
f(&mut r);
|
f(&mut r);
|
||||||
let res: Vec<raw::Score> = r.build(self).await?.json().await?;
|
r.build(self).await
|
||||||
let mut res: Vec<Score> = vec_try_into(res)?;
|
|
||||||
|
|
||||||
// with a scores request you need to fill the beatmap ids yourself
|
|
||||||
res.iter_mut().for_each(|v| {
|
|
||||||
v.beatmap_id = beatmap_id;
|
|
||||||
});
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_best(
|
pub async fn user_best(
|
||||||
|
|
|
@ -558,6 +558,8 @@ pub struct Score {
|
||||||
pub count_geki: u64,
|
pub count_geki: u64,
|
||||||
pub max_combo: u64,
|
pub max_combo: u64,
|
||||||
pub perfect: bool,
|
pub perfect: bool,
|
||||||
|
|
||||||
|
pub lazer_build_id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Score {
|
impl Score {
|
||||||
|
|
|
@ -40,6 +40,10 @@ bitflags::bitflags! {
|
||||||
const NOVIDEO = Self::TD.bits; /* never forget */
|
const NOVIDEO = Self::TD.bits; /* never forget */
|
||||||
const SPEED_CHANGING = Self::DT.bits | Self::HT.bits | Self::NC.bits;
|
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 MAP_CHANGING = Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits;
|
||||||
|
|
||||||
|
// Made up flags
|
||||||
|
const CLASSIC = 1 << 59;
|
||||||
|
const UNKNOWN = 1 << 60;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +72,8 @@ const MODS_WITH_NAMES: &[(Mods, &str)] = &[
|
||||||
(Mods::KEY7, "7K"),
|
(Mods::KEY7, "7K"),
|
||||||
(Mods::KEY8, "8K"),
|
(Mods::KEY8, "8K"),
|
||||||
(Mods::KEY9, "9K"),
|
(Mods::KEY9, "9K"),
|
||||||
|
(Mods::CLASSIC, "CL"),
|
||||||
|
(Mods::UNKNOWN, "??"),
|
||||||
];
|
];
|
||||||
|
|
||||||
impl std::str::FromStr for Mods {
|
impl std::str::FromStr for Mods {
|
||||||
|
@ -106,6 +112,8 @@ impl std::str::FromStr for Mods {
|
||||||
"7K" => res |= Mods::KEY7,
|
"7K" => res |= Mods::KEY7,
|
||||||
"8K" => res |= Mods::KEY8,
|
"8K" => res |= Mods::KEY8,
|
||||||
"9K" => res |= Mods::KEY9,
|
"9K" => res |= Mods::KEY9,
|
||||||
|
"CL" => res |= Mods::CLASSIC,
|
||||||
|
"??" => res |= Mods::UNKNOWN,
|
||||||
v => return Err(format!("{} is not a valid mod", v)),
|
v => return Err(format!("{} is not a valid mod", v)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,8 @@ impl TryFrom<raw::Score> for Score {
|
||||||
count_geki: parse_from_str(&raw.countgeki)?,
|
count_geki: parse_from_str(&raw.countgeki)?,
|
||||||
max_combo: parse_from_str(&raw.maxcombo)?,
|
max_combo: parse_from_str(&raw.maxcombo)?,
|
||||||
perfect: parse_bool(&raw.perfect)?,
|
perfect: parse_bool(&raw.perfect)?,
|
||||||
|
|
||||||
|
lazer_build_id: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use rosu_v2::model as rosu;
|
use rosu_v2::model::{
|
||||||
|
self as rosu,
|
||||||
|
mods::{GameModIntermode, GameModsIntermode},
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -25,7 +28,10 @@ fn time_to_utc(s: time::OffsetDateTime) -> DateTime<Utc> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
pub(crate) fn from_rosu(bm: rosu::beatmap::Beatmap, set: &rosu::beatmap::Beatmapset) -> Self {
|
pub(crate) fn from_rosu(
|
||||||
|
bm: rosu::beatmap::BeatmapExtended,
|
||||||
|
set: &rosu::beatmap::BeatmapsetExtended,
|
||||||
|
) -> Self {
|
||||||
let last_updated = time_to_utc(bm.last_updated);
|
let last_updated = time_to_utc(bm.last_updated);
|
||||||
let difficulty = Difficulty::from_rosu(&bm);
|
let difficulty = Difficulty::from_rosu(&bm);
|
||||||
Self {
|
Self {
|
||||||
|
@ -68,7 +74,7 @@ impl Beatmap {
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub(crate) fn from_rosu(
|
pub(crate) fn from_rosu(
|
||||||
user: rosu::user::User,
|
user: rosu::user::UserExtended,
|
||||||
stats: rosu::user::UserStatistics,
|
stats: rosu::user::UserStatistics,
|
||||||
events: Vec<rosu::recent_event::RecentEvent>,
|
events: Vec<rosu::recent_event::RecentEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -129,8 +135,39 @@ impl From<rosu::recent_event::RecentEvent> for UserEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<rosu::score::Score> for Score {
|
||||||
|
fn from(s: rosu::score::Score) -> Self {
|
||||||
|
let legacy_stats = s.statistics.as_legacy(s.mode);
|
||||||
|
Self {
|
||||||
|
id: Some(s.id),
|
||||||
|
user_id: s.user_id as u64,
|
||||||
|
date: time_to_utc(s.ended_at),
|
||||||
|
replay_available: s.replay,
|
||||||
|
beatmap_id: s.map_id as u64,
|
||||||
|
score: s.score as u64,
|
||||||
|
pp: s.pp.map(|v| v as f64),
|
||||||
|
rank: s.grade.into(),
|
||||||
|
mods: s
|
||||||
|
.mods
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.intermode())
|
||||||
|
.collect::<GameModsIntermode>()
|
||||||
|
.into(),
|
||||||
|
count_300: legacy_stats.count_300 as u64,
|
||||||
|
count_100: legacy_stats.count_100 as u64,
|
||||||
|
count_50: legacy_stats.count_50 as u64,
|
||||||
|
count_miss: legacy_stats.count_miss as u64,
|
||||||
|
count_katu: legacy_stats.count_katu as u64,
|
||||||
|
count_geki: legacy_stats.count_geki as u64,
|
||||||
|
max_combo: s.max_combo as u64,
|
||||||
|
perfect: s.is_perfect_combo,
|
||||||
|
lazer_build_id: s.build_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Difficulty {
|
impl Difficulty {
|
||||||
pub(crate) fn from_rosu(bm: &rosu::beatmap::Beatmap) -> Self {
|
pub(crate) fn from_rosu(bm: &rosu::beatmap::BeatmapExtended) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stars: bm.stars as f64,
|
stars: bm.stars as f64,
|
||||||
aim: None,
|
aim: None,
|
||||||
|
@ -214,3 +251,95 @@ impl From<rosu::beatmap::Language> for Language {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<rosu::Grade> for Rank {
|
||||||
|
fn from(value: rosu::Grade) -> Self {
|
||||||
|
match value {
|
||||||
|
rosu::Grade::F => Rank::F,
|
||||||
|
rosu::Grade::D => Rank::D,
|
||||||
|
rosu::Grade::C => Rank::C,
|
||||||
|
rosu::Grade::B => Rank::B,
|
||||||
|
rosu::Grade::A => Rank::A,
|
||||||
|
rosu::Grade::S => Rank::S,
|
||||||
|
rosu::Grade::SH => Rank::SH,
|
||||||
|
rosu::Grade::X => Rank::SS,
|
||||||
|
rosu::Grade::XH => Rank::SSH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Mods> for rosu::mods::GameModsIntermode {
|
||||||
|
fn from(value: Mods) -> Self {
|
||||||
|
let mut res = GameModsIntermode::new();
|
||||||
|
const MOD_MAP: &[(Mods, GameModIntermode)] = &[
|
||||||
|
(Mods::NF, GameModIntermode::NoFail),
|
||||||
|
(Mods::EZ, GameModIntermode::Easy),
|
||||||
|
(Mods::TD, GameModIntermode::TouchDevice),
|
||||||
|
(Mods::HD, GameModIntermode::Hidden),
|
||||||
|
(Mods::HR, GameModIntermode::HardRock),
|
||||||
|
(Mods::SD, GameModIntermode::SuddenDeath),
|
||||||
|
(Mods::DT, GameModIntermode::DoubleTime),
|
||||||
|
(Mods::RX, GameModIntermode::Relax),
|
||||||
|
(Mods::HT, GameModIntermode::HalfTime),
|
||||||
|
(Mods::NC, GameModIntermode::Nightcore),
|
||||||
|
(Mods::FL, GameModIntermode::Flashlight),
|
||||||
|
(Mods::AT, GameModIntermode::Autoplay),
|
||||||
|
(Mods::SO, GameModIntermode::SpunOut),
|
||||||
|
(Mods::AP, GameModIntermode::Autopilot),
|
||||||
|
(Mods::PF, GameModIntermode::Perfect),
|
||||||
|
(Mods::KEY1, GameModIntermode::OneKey),
|
||||||
|
(Mods::KEY2, GameModIntermode::TwoKeys),
|
||||||
|
(Mods::KEY3, GameModIntermode::ThreeKeys),
|
||||||
|
(Mods::KEY4, GameModIntermode::FourKeys),
|
||||||
|
(Mods::KEY5, GameModIntermode::FiveKeys),
|
||||||
|
(Mods::KEY6, GameModIntermode::SixKeys),
|
||||||
|
(Mods::KEY7, GameModIntermode::SevenKeys),
|
||||||
|
(Mods::KEY8, GameModIntermode::EightKeys),
|
||||||
|
(Mods::KEY9, GameModIntermode::NineKeys),
|
||||||
|
];
|
||||||
|
for (m1, m2) in MOD_MAP {
|
||||||
|
if value.contains(*m1) {
|
||||||
|
res.insert(*m2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rosu::mods::GameModsIntermode> for Mods {
|
||||||
|
fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self {
|
||||||
|
value
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| match m {
|
||||||
|
GameModIntermode::NoFail => Mods::NF,
|
||||||
|
GameModIntermode::Easy => Mods::EZ,
|
||||||
|
GameModIntermode::TouchDevice => Mods::TD,
|
||||||
|
GameModIntermode::Hidden => Mods::HD,
|
||||||
|
GameModIntermode::HardRock => Mods::HR,
|
||||||
|
GameModIntermode::SuddenDeath => Mods::SD,
|
||||||
|
GameModIntermode::DoubleTime => Mods::DT,
|
||||||
|
GameModIntermode::Relax => Mods::RX,
|
||||||
|
GameModIntermode::HalfTime => Mods::HT,
|
||||||
|
GameModIntermode::Nightcore => Mods::NC,
|
||||||
|
GameModIntermode::Flashlight => Mods::FL,
|
||||||
|
GameModIntermode::Autoplay => Mods::AT,
|
||||||
|
GameModIntermode::SpunOut => Mods::SO,
|
||||||
|
GameModIntermode::Autopilot => Mods::AP,
|
||||||
|
GameModIntermode::Perfect => Mods::PF,
|
||||||
|
GameModIntermode::OneKey => Mods::KEY1,
|
||||||
|
GameModIntermode::TwoKeys => Mods::KEY2,
|
||||||
|
GameModIntermode::ThreeKeys => Mods::KEY3,
|
||||||
|
GameModIntermode::FourKeys => Mods::KEY4,
|
||||||
|
GameModIntermode::FiveKeys => Mods::KEY5,
|
||||||
|
GameModIntermode::SixKeys => Mods::KEY6,
|
||||||
|
GameModIntermode::SevenKeys => Mods::KEY7,
|
||||||
|
GameModIntermode::EightKeys => Mods::KEY8,
|
||||||
|
GameModIntermode::NineKeys => Mods::KEY9,
|
||||||
|
GameModIntermode::Classic => Mods::CLASSIC,
|
||||||
|
_ => Mods::UNKNOWN,
|
||||||
|
})
|
||||||
|
.fold(Mods::NOMOD, |a, b| a | b)
|
||||||
|
|
||||||
|
// Mods::from_bits_truncate(value.bits() as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ fn handle_not_found<T>(v: Result<T, OsuError>) -> Result<Option<T>, OsuError> {
|
||||||
|
|
||||||
pub mod builders {
|
pub mod builders {
|
||||||
use reqwest::Response;
|
use reqwest::Response;
|
||||||
|
use rosu_v2::model::mods::GameModsIntermode;
|
||||||
|
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
|
||||||
|
@ -213,19 +214,6 @@ pub mod builders {
|
||||||
events.retain(|e| (now <= e.created_at));
|
events.retain(|e| (now <= e.created_at));
|
||||||
let stats = user.statistics.take().unwrap();
|
let stats = user.statistics.take().unwrap();
|
||||||
Ok(Some(models::User::from_rosu(user, stats, events)))
|
Ok(Some(models::User::from_rosu(user, stats, events)))
|
||||||
// Ok(client
|
|
||||||
// .build_request("https://osu.ppy.sh/api/get_user")
|
|
||||||
// .await?
|
|
||||||
// .query(&self.user.to_query())
|
|
||||||
// .query(&self.mode.to_query())
|
|
||||||
// .query(
|
|
||||||
// &self
|
|
||||||
// .event_days
|
|
||||||
// .map(|v| ("event_days", v.to_string()))
|
|
||||||
// .to_query(),
|
|
||||||
// )
|
|
||||||
// .send()
|
|
||||||
// .await?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,17 +256,40 @@ pub mod builders {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn build(&self, client: &Client) -> Result<Response> {
|
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Score>> {
|
||||||
Ok(client
|
let scores = handle_not_found(match self.user {
|
||||||
.build_request("https://osu.ppy.sh/api/get_scores")
|
Some(user) => {
|
||||||
.await?
|
let mut r = client
|
||||||
.query(&[("b", self.beatmap_id)])
|
.rosu
|
||||||
.query(&self.user.to_query())
|
.beatmap_user_scores(self.beatmap_id as u32, user);
|
||||||
.query(&self.mode.to_query())
|
if let Some(mode) = self.mode {
|
||||||
.query(&self.mods.to_query())
|
r = r.mode(mode.into());
|
||||||
.query(&self.limit.map(|v| ("limit", v.to_string())).to_query())
|
}
|
||||||
.send()
|
match self.mods {
|
||||||
.await?)
|
Some(mods) => r.await.map(|mut ss| {
|
||||||
|
let mods = GameModsIntermode::from(mods);
|
||||||
|
ss.retain(|s| mods.iter().all(|m| s.mods.contains_intermode(m)));
|
||||||
|
ss
|
||||||
|
}),
|
||||||
|
None => r.await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let mut r = client.rosu.beatmap_scores(self.beatmap_id as u32).global();
|
||||||
|
if let Some(mode) = self.mode {
|
||||||
|
r = r.mode(mode.into());
|
||||||
|
}
|
||||||
|
if let Some(mods) = self.mods {
|
||||||
|
r = r.mods(GameModsIntermode::from(mods));
|
||||||
|
}
|
||||||
|
if let Some(limit) = self.limit {
|
||||||
|
r = r.limit(limit as u32);
|
||||||
|
}
|
||||||
|
r.await
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.unwrap_or(vec![]);
|
||||||
|
Ok(scores.into_iter().map(|v| v.into()).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue