From 8fa566754e1b6714ba473ca0c5e79427577b350a Mon Sep 17 00:00:00 2001 From: Tracreed Date: Thu, 4 Mar 2021 13:35:55 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 + Cargo.toml | 17 +++++ src/client.rs | 101 +++++++++++++++++++++++++++++ src/lib.rs | 34 ++++++++++ src/types.rs | 0 src/user.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 326 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/client.rs create mode 100644 src/lib.rs create mode 100644 src/types.rs create mode 100644 src/user.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e551aa3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.env diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3765876 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "osu_v2" +version = "0.1.0" +authors = ["Tracreed "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dotenv = "0.15" +thiserror = "1.0" +reqwest = { version = "0.11", features = ["json", "blocking"] } +tokio = { version = "1", features = ["full"] } +actix-rt = "*" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +async-trait = "0.1.42" \ No newline at end of file diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..e664411 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,101 @@ +use super::user::*; +use super::Error; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Client { + client_id: String, + client_secret: String, + client_token: String, + base_url: String, +} +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct AccessToken { + token_type: String, + expires_in: u64, + access_token: String, +} + +impl<'a> Client { + pub async fn new, CS: Into>( + client_id: CI, + client_secret: CS, + ) -> Result { + let mut client = Client { + client_id: client_id.into(), + client_secret: client_secret.into(), + base_url: "https://osu.ppy.sh/api/v2".to_string(), + client_token: "".to_string(), + }; + client.refresh_token().await.unwrap(); + Ok(client) + } + async fn get(&mut self, url: String, params: T) -> Result + where + T: Into> + { + let cli = reqwest::Client::new(); + let parms = params.into(); + let resp = match cli.get(&format!("{}{}", &self.base_url, &url).to_string()) + .bearer_auth(&self.client_token).query(&parms).send().await { + Ok(v) => { + if v.status().is_client_error() { + self.refresh_token().await.unwrap(); + cli.get(&format!("{}{}", &self.base_url, &url).to_string()) + .bearer_auth(&self.client_token).query(&parms).send().await.unwrap() + } else { + v + } + }, + Err(why) => {return Err(Error::NotAuthenticated(why.to_string()))}, + }; + Ok(resp) + } + async fn refresh_token(&mut self) -> Result<(), Error> { + let client = reqwest::Client::new(); + let mut data = HashMap::new(); + data.insert("client_id", self.client_id.clone()); + data.insert("client_secret", self.client_secret.clone()); + data.insert("grant_type", "client_credentials".to_string()); + data.insert("scope", "public".to_string()); + let token = match client + .post("https://osu.ppy.sh/oauth/token") + .json(&data) + .send() + .await + { + Ok(a) => a, + Err(why) => return Err(Error::NotAuthenticated(why.to_string())), + } + .json::() + .await.unwrap(); + self.client_token = token.access_token; + Ok(()) + } +} + +#[async_trait] +impl UserMethods for Client { + async fn get_user<'a>(&'a mut self, id: u64, m: String) -> Result, Error> { + let resp = self + .get(format!("/users/{}/{}", id, m), None) + .await + .unwrap(); + let user = resp.json::().await.unwrap(); + Ok(Box::new(user)) + //Ok(()) + } + async fn search_user<'a>(&'a mut self, name: String) -> Result, Error> { + let paras = [("mode", "user"), ("query", &name)]; + let resp = self + .get("/search".to_string(), ¶s[..]) + .await + .unwrap(); + let users = resp.json::().await.unwrap(); + Ok(Box::new(users)) + } +} + +//fn get() diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b22a79f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,34 @@ +pub mod client; +pub mod user; +use thiserror::Error; +#[derive(Error, Debug)] +pub enum Error { + #[error("Couldn't authenticate")] + NotAuthenticated(String), +} +#[cfg(test)] +mod tests { + use super::client::*; + use std::env; + extern crate dotenv; + use crate::user::UserMethods; + use std::mem::size_of; + use crate::user::User; + #[actix_rt::test] + async fn it_works() { + assert_eq!(2 + 2, 4); + dotenv::dotenv().expect("Failed to load .env file"); + let client_id = env::var("OSU_ID").expect("Expected a token in the environment"); + let client_secret = env::var("OSU_SECRET").expect("Expected a token in the environment"); + let mut client = Client::new(client_id, client_secret).await.unwrap(); + let users = client.search_user("beetroot".to_string()).await.unwrap(); + let user = client + .get_user(users.user.data[0].id, "osu".to_string()) + .await + .unwrap(); + println!("{:?}", client); + println!("{:?}", users.user.data[0]); + println!("{:?}", user); + println!("{}", size_of::()) + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..dd549c2 --- /dev/null +++ b/src/user.rs @@ -0,0 +1,171 @@ +use super::Error; +use async_trait::async_trait; +use serde::{Deserialize, Deserializer}; +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct User { + pub id: i64, + pub username: String, + pub profile_colour: Option, + pub avatar_url: String, + pub country_code: String, + pub default_group: String, + pub is_active: bool, + pub is_bot: bool, + pub is_deleted: bool, + pub is_online: bool, + pub is_supporter: bool, + pub last_visit: Option, + pub pm_friends_only: bool, + pub cover_url: String, + pub discord: Option, + pub has_supported: bool, + pub interests: Option, + pub join_date: String, + pub kudosu: Kudosu, + pub location: Option, + pub max_blocks: u64, + pub max_friends: u64, + pub occupation: Option, + pub playmode: String, + pub playstyle: Option>, + pub post_count: u64, + pub profile_order: Vec, + pub skype: Option, + pub title: Option, + pub twitter: Option, + pub website: Option, + pub country: Country, + pub cover: Cover, + pub is_admin: Option, + pub is_bng: Option, + pub is_full_bn: Option, + pub is_gmt: Option, + pub is_limited_bn: Option, + pub is_moderator: Option, + pub is_nat: Option, + pub is_restricted: Option, + pub is_silenced: Option, + pub statistics: UserStatistics, + pub rank_history: Option, + pub user_achivements: Option>, + pub beatmap_playcounts_count: u64, + pub favourite_beatmapset_count: u64, + pub follower_count: u64, + pub graveyard_beatmapset_count: u64, + pub monthly_playcounts: Option>, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct CompactUser { + pub avatar_url: String, + pub country_code: String, + pub default_group: String, + pub id: u64, + pub is_active: bool, + pub is_bot: bool, + pub is_deleted: bool, + pub is_online: bool, + pub is_supporter: bool, + pub last_visit: Option, + pub pm_friends_only: bool, + pub profile_colour: Option, + pub username: String, +} + +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct Kudosu { + pub total: u64, + pub available: u64, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct RankHistory { + pub mode: String, + pub data: Vec, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct MonthlyPlayCount { + pub start_date: String, + pub count: u64, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct UserAchivement { + pub achieved_at: String, + pub achievement_id: u32, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct SearchResult { + pub user: SearchData, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct SearchData { + pub data: Vec, + pub total: u32, +} +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct UserStatistics { + pub grade_counts: GradeCounts, + pub hit_accuracy: f32, + pub is_ranked: bool, + pub level: Level, + pub maximum_combo: u64, + pub play_count: u64, + pub play_time: Option, + pub pp: f32, + pub ranked_score: u64, + pub replays_watched_by_others: u64, + pub total_hits: u64, + pub total_score: u64, + pub rank: UserRank, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct UserRank { + pub global: Option, + pub country: Option, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct Level { + pub current: u8, + pub progress: u16, +} +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct GradeCounts { + pub a: u64, + pub s: u64, + pub sh: u64, + pub ss: u64, + pub ssh: u64, +} + +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct Country { + pub code: String, + pub name: String, +} + +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct Cover { + custom_url: Option, + url: String, + #[serde(default)] + #[serde(deserialize_with = "parse_to_int")] + id: Option, +} + +fn parse_to_int<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let stri: Option = Option::deserialize(deserializer)?; + if let Some(s) = stri { + match s.parse::() { + Ok(v) => return Ok(Some(v)), + Err(_) => return Ok(None), + } + } + Ok(None) +} + +#[async_trait] +pub trait UserMethods { + async fn get_user<'a>(&'a mut self, id: u64, m: String) -> Result, Error>; + async fn search_user<'a>(&'a mut self, name: String) -> Result, Error>; +}