diff --git a/Cargo.lock b/Cargo.lock index 8efe5f8..0420ed5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3087,7 +3087,6 @@ dependencies = [ "osuparse", "rand", "regex", - "reqwest", "rosu-pp", "rosu-v2", "serde", diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 702f704..04db150 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -74,8 +74,6 @@ pub async fn setup( let mk_osu_client = || async { Arc::new( OsuHttpClient::new( - std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."), - http_client.clone(), std::env::var("OSU_API_CLIENT_ID") .expect("Please set OSU_API_CLIENT_ID as osu! api v2 client ID.") .parse() diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index 5d97321..8c36643 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -8,18 +8,11 @@ mod test; use models::*; use request::builders::*; use request::*; -use reqwest::Client as HTTPClient; use std::convert::TryInto; -use youmubot_prelude::{ratelimit::Ratelimit, *}; - -/// The number of requests per minute to the osu! server. -const REQUESTS_PER_MINUTE: usize = 100; +use youmubot_prelude::*; /// Client is the client that will perform calls to the osu! api server. pub struct Client { - client: Ratelimit, - key: String, - rosu: rosu_v2::Osu, } @@ -35,32 +28,13 @@ pub fn vec_try_into>(v: Vec) -> Result, impl Client { /// Create a new client from the given API key. - pub async fn new( - key: String, - client: HTTPClient, - client_id: u64, - client_secret: impl Into, - ) -> Result { - let client = Ratelimit::new( - client, - REQUESTS_PER_MINUTE, - std::time::Duration::from_secs(60), - ); + pub async fn new(client_id: u64, client_secret: impl Into) -> Result { let rosu = rosu_v2::OsuBuilder::new() .client_id(client_id) .client_secret(client_secret) .build() .await?; - Ok(Client { client, key, rosu }) - } - - pub(crate) async fn build_request(&self, url: &str) -> Result { - Ok(self - .client - .borrow() - .await? - .get(url) - .query(&[("k", &*self.key)])) + Ok(Client { rosu }) } pub async fn beatmaps( diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 1d866e0..165f79b 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -1,13 +1,10 @@ use chrono::{DateTime, Utc}; -use regex::Regex; use rosu_pp::GameMode; use serde::{Deserialize, Serialize}; use std::fmt; use std::time::Duration; pub mod mods; -pub mod parse; -pub(crate) mod raw; pub(crate) mod rosu; pub use mods::Mods; diff --git a/youmubot-osu/src/models/parse.rs b/youmubot-osu/src/models/parse.rs deleted file mode 100644 index a35664a..0000000 --- a/youmubot-osu/src/models/parse.rs +++ /dev/null @@ -1,342 +0,0 @@ -use super::*; -use chrono::{ - format::{parse, Item, Numeric, Pad, Parsed}, - DateTime, ParseError as ChronoParseError, Utc, -}; -use std::convert::TryFrom; -use std::time::Duration; -use std::{error::Error, fmt, str::FromStr}; - -lazy_static::lazy_static! { - static ref EVENT_RANK_REGEX: Regex = Regex::new(r#"^.+achieved .*rank #(\d+).* on .+\((.+)\)$"#).unwrap(); -} - -/// Errors that can be identified from parsing. -#[derive(Debug)] -pub enum ParseError { - InvalidValue { field: &'static str, value: String }, - FromStr(String), - NoApprovalDate, - NotUserEventRank, - DateParseError(ChronoParseError), -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use ParseError::*; - match self { - InvalidValue { - ref field, - ref value, - } => write!(f, "Invalid value `{}` for {}", value, field), - FromStr(ref s) => write!(f, "Invalid value `{}` parsing from string", s), - NoApprovalDate => write!(f, "Approval date expected but not found"), - NotUserEventRank => write!(f, "Trying to parse user event as UserEventRank"), - DateParseError(ref r) => write!(f, "Error parsing date: {}", r), - } - } -} - -impl Error for ParseError {} - -type ParseResult = Result; - -impl TryFrom for Score { - type Error = ParseError; - fn try_from(raw: raw::Score) -> Result { - Ok(Score { - id: raw.score_id.map(parse_from_str).transpose()?, - user_id: parse_from_str(&raw.user_id)?, - date: parse_date(&raw.date)?, - beatmap_id: raw.beatmap_id.map(parse_from_str).transpose()?.unwrap_or(0), - replay_available: raw - .replay_available - .map(parse_bool) - .transpose()? - .unwrap_or(false), - score: parse_from_str(&raw.score)?, - pp: raw.pp.map(parse_from_str).transpose()?, - rank: parse_from_str(&raw.rank)?, - mods: { - let v: u64 = parse_from_str(&raw.enabled_mods)?; - Mods::from_bits(v).unwrap_or(Mods::NOMOD) - }, - 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)?, - - lazer_build_id: None, - }) - } -} - -impl TryFrom for User { - type Error = ParseError; - fn try_from(raw: raw::User) -> Result { - Ok(User { - id: parse_from_str(&raw.user_id)?, - username: raw.username, - joined: parse_date(&raw.join_date)?, - country: raw.country, - count_300: raw.count300.map(parse_from_str).unwrap_or(Ok(0))?, - count_100: raw.count100.map(parse_from_str).unwrap_or(Ok(0))?, - count_50: raw.count50.map(parse_from_str).unwrap_or(Ok(0))?, - play_count: raw.playcount.map(parse_from_str).unwrap_or(Ok(0))?, - played_time: raw - .total_seconds_played - .map(parse_duration) - .unwrap_or_else(|| Ok(Duration::from_secs(0)))?, - ranked_score: raw.ranked_score.map(parse_from_str).unwrap_or(Ok(0))?, - total_score: raw.total_score.map(parse_from_str).unwrap_or(Ok(0))?, - count_ss: raw.count_rank_ss.map(parse_from_str).unwrap_or(Ok(0))?, - count_ssh: raw.count_rank_ssh.map(parse_from_str).unwrap_or(Ok(0))?, - count_s: raw.count_rank_s.map(parse_from_str).unwrap_or(Ok(0))?, - count_sh: raw.count_rank_sh.map(parse_from_str).unwrap_or(Ok(0))?, - count_a: raw.count_rank_a.map(parse_from_str).unwrap_or(Ok(0))?, - rank: raw.pp_rank.map(parse_from_str).unwrap_or(Ok(0))?, - country_rank: raw.pp_country_rank.map(parse_from_str).unwrap_or(Ok(0))?, - level: raw.level.map(parse_from_str).unwrap_or(Ok(0.0))?, - pp: Some(raw.pp_raw.map(parse_from_str).unwrap_or(Ok(0.0))?).filter(|v| *v != 0.0), - accuracy: raw.accuracy.map(parse_from_str).unwrap_or(Ok(0.0))?, - events: { - let mut v = Vec::new(); - for e in raw.events.into_iter() { - v.push(parse_user_event(e)?); - } - v - }, - }) - } -} - -impl TryFrom for Beatmap { - type Error = ParseError; - fn try_from(raw: raw::Beatmap) -> Result { - Ok(Beatmap { - approval: parse_approval_status(&raw)?, - submit_date: parse_date(&raw.submit_date)?, - last_update: parse_date(&raw.last_update)?, - download_available: !(parse_bool(&raw.download_unavailable)?), - audio_available: !(parse_bool(&raw.audio_unavailable)?), - artist: raw.artist, - beatmap_id: parse_from_str(&raw.beatmap_id)?, - beatmapset_id: parse_from_str(&raw.beatmapset_id)?, - title: raw.title, - creator: raw.creator, - creator_id: parse_from_str(&raw.creator_id)?, - 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: 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)?, - hp: parse_from_str(&raw.diff_drain)?, - 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: raw.max_combo.map(parse_from_str).transpose()?, - bpm: parse_from_str(&raw.bpm)?, - drain_length: parse_duration(&raw.hit_length)?, - total_length: parse_duration(&raw.total_length)?, - }, - file_hash: raw.file_md5, - mode: parse_mode(&raw.mode)?, - favourite_count: parse_from_str(&raw.favourite_count)?, - rating: parse_from_str(&raw.rating)?, - play_count: parse_from_str(&raw.playcount)?, - pass_count: parse_from_str(&raw.passcount)?, - }) - } -} - -fn parse_user_event(s: raw::UserEvent) -> ParseResult { - match parse_user_event_rank(&s) { - Ok(r) => return Ok(UserEvent::Rank(r)), - Err(_) => (), - }; - Ok(UserEvent::OtherV1 { - display_html: s.display_html, - date: parse_date(&s.date)?, - epic_factor: parse_from_str(&s.epicfactor)?, - }) -} - -fn parse_user_event_rank(s: &raw::UserEvent) -> ParseResult { - let captures = EVENT_RANK_REGEX - .captures(s.display_html.as_str()) - .ok_or(ParseError::NotUserEventRank)?; - let rank: u16 = captures - .get(1) - .ok_or(ParseError::NotUserEventRank)? - .as_str() - .parse() - .map_err(|_| ParseError::NotUserEventRank)?; - let mode = super::Mode::parse_from_display( - captures - .get(2) - .ok_or(ParseError::NotUserEventRank)? - .as_str(), - ) - .ok_or(ParseError::NotUserEventRank)?; - let beatmap_id = s - .beatmap_id - .as_ref() - .ok_or(ParseError::NotUserEventRank)? - .parse::() - .map_err(|_| ParseError::NotUserEventRank)?; - Ok(UserEventRank { - beatmap_id, - date: parse_date(&s.date)?, - mode, - rank, - }) -} - -fn parse_mode(s: impl AsRef) -> ParseResult { - let t: u8 = parse_from_str(s)?; - use Mode::*; - Ok(match t { - 0 => Std, - 1 => Taiko, - 2 => Catch, - 3 => Mania, - _ => { - return Err(ParseError::InvalidValue { - field: "mode", - value: t.to_string(), - }) - } - }) -} - -fn parse_language(s: impl AsRef) -> ParseResult { - let t: u8 = parse_from_str(s)?; - use Language::*; - Ok(match t { - 0 => Any, - 1 | 14 => Other, - 2 => English, - 3 => Japanese, - 4 => Chinese, - 5 => Instrumental, - 6 => Korean, - 7 => French, - 8 => German, - 9 => Swedish, - 10 => Spanish, - 11 => Italian, - 12 => Russian, - 13 => Polish, - _ => { - return Err(ParseError::InvalidValue { - field: "language", - value: t.to_string(), - }) - } - }) -} - -fn parse_genre(s: impl AsRef) -> ParseResult { - let t: u8 = parse_from_str(s)?; - use Genre::*; - Ok(match t { - 0 => Any, - 1 => Unspecified, - 2 => VideoGame, - 3 => Anime, - 4 => Rock, - 5 => Pop, - 6 => Other, - 7 => Novelty, - 9 => HipHop, - 10 => Electronic, - 11 => Metal, - 12 => Classical, - 13 => Folk, - 14 => Jazz, - _ => { - return Err(ParseError::InvalidValue { - field: "genre", - value: t.to_string(), - }) - } - }) -} - -fn parse_duration(s: impl AsRef) -> ParseResult { - Ok(Duration::from_secs(parse_from_str(s)?)) -} - -fn parse_from_str(s: impl AsRef) -> ParseResult { - let v = s.as_ref(); - T::from_str(v).map_err(|_| ParseError::FromStr(v.to_owned())) -} - -fn parse_bool(b: impl AsRef) -> ParseResult { - match b.as_ref() { - "1" => Ok(true), - "0" => Ok(false), - t => Err(ParseError::InvalidValue { - field: "bool", - value: t.to_owned(), - }), - } -} - -fn parse_approval_status(b: &raw::Beatmap) -> ParseResult { - use ApprovalStatus::*; - Ok(match &b.approved[..] { - "4" => Loved, - "3" => Qualified, - "2" => Approved, - "1" => Ranked(parse_date( - b.approved_date.as_ref().ok_or(ParseError::NoApprovalDate)?, - )?), - "0" => Pending, - "-1" => WIP, - "-2" => Graveyarded, - t => { - return Err(ParseError::InvalidValue { - field: "approval status", - value: t.to_owned(), - }) - } - }) -} - -fn parse_date(date: impl AsRef) -> ParseResult> { - let mut parsed = Parsed::new(); - parse( - &mut parsed, - date.as_ref(), - [ - Item::Numeric(Numeric::Year, Pad::Zero), - Item::Literal("-"), - Item::Numeric(Numeric::Month, Pad::Zero), - Item::Literal("-"), - Item::Numeric(Numeric::Day, Pad::Zero), - Item::Space(""), - Item::Numeric(Numeric::Hour, Pad::Zero), - Item::Literal(":"), - Item::Numeric(Numeric::Minute, Pad::Zero), - Item::Literal(":"), - Item::Numeric(Numeric::Second, Pad::Zero), - ] - .iter(), - ) - .map_err(ParseError::DateParseError)?; - parsed - .to_datetime_with_timezone(&Utc {}) - .map_err(ParseError::DateParseError) -} diff --git a/youmubot-osu/src/models/raw.rs b/youmubot-osu/src/models/raw.rs deleted file mode 100644 index cb42842..0000000 --- a/youmubot-osu/src/models/raw.rs +++ /dev/null @@ -1,97 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize, Clone, Debug)] -pub(crate) struct Beatmap { - pub approved: String, - pub submit_date: String, - pub approved_date: Option, - pub last_update: String, - pub artist: String, - pub beatmap_id: String, - pub beatmapset_id: String, - pub bpm: String, - pub creator: String, - pub creator_id: String, - pub difficultyrating: String, - pub diff_aim: Option, - pub diff_speed: Option, - pub diff_size: String, - pub diff_overall: String, - pub diff_approach: String, - pub diff_drain: String, - pub hit_length: String, - pub source: Option, - pub genre_id: String, - pub language_id: String, - pub title: String, - pub total_length: String, - pub version: String, - pub file_md5: String, - pub mode: String, - pub tags: String, - pub favourite_count: String, - pub rating: String, - pub playcount: String, - pub passcount: String, - pub count_normal: String, - pub count_slider: String, - pub count_spinner: String, - pub max_combo: Option, - pub download_unavailable: String, - pub audio_unavailable: String, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct User { - pub user_id: String, - pub username: String, - pub join_date: String, - pub country: String, - pub count300: Option, - pub count100: Option, - pub count50: Option, - pub playcount: Option, - pub ranked_score: Option, - pub total_score: Option, - pub pp_rank: Option, - pub level: Option, - pub pp_raw: Option, - pub accuracy: Option, - pub count_rank_ss: Option, - pub count_rank_ssh: Option, - pub count_rank_s: Option, - pub count_rank_sh: Option, - pub count_rank_a: Option, - pub total_seconds_played: Option, - pub pp_country_rank: Option, - pub events: Vec, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct UserEvent { - pub display_html: String, - pub beatmap_id: Option, - pub date: String, - pub epicfactor: String, -} - -#[derive(Deserialize, Debug)] -pub(crate) struct Score { - pub score_id: Option, - pub beatmap_id: Option, - pub score: 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: Option, - pub replay_available: Option, -} diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index dbc1626..bd47ee4 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -109,7 +109,6 @@ fn handle_not_found(v: Result) -> Result, OsuError> { } pub mod builders { - use reqwest::Response; use rosu_v2::model::mods::GameModsIntermode; use crate::models;