Initial commit

This commit is contained in:
David Alasow 2021-03-04 13:35:55 +01:00
commit 8fa566754e
6 changed files with 326 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
Cargo.lock
.env

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "osu_v2"
version = "0.1.0"
authors = ["Tracreed <davidalasow@gmail.com>"]
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"

101
src/client.rs Normal file
View File

@ -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<CI: Into<String>, CS: Into<String>>(
client_id: CI,
client_secret: CS,
) -> Result<self::Client, Error> {
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<T>(&mut self, url: String, params: T) -> Result<reqwest::Response, Error>
where
T: Into<Option<&'a[(&'a str, &'a str)]>>
{
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::<AccessToken>()
.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<Box<User>, Error> {
let resp = self
.get(format!("/users/{}/{}", id, m), None)
.await
.unwrap();
let user = resp.json::<User>().await.unwrap();
Ok(Box::new(user))
//Ok(())
}
async fn search_user<'a>(&'a mut self, name: String) -> Result<Box<SearchResult>, Error> {
let paras = [("mode", "user"), ("query", &name)];
let resp = self
.get("/search".to_string(), &paras[..])
.await
.unwrap();
let users = resp.json::<SearchResult>().await.unwrap();
Ok(Box::new(users))
}
}
//fn get()

34
src/lib.rs Normal file
View File

@ -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::<User>())
}
}

0
src/types.rs Normal file
View File

171
src/user.rs Normal file
View File

@ -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<String>,
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<String>,
pub pm_friends_only: bool,
pub cover_url: String,
pub discord: Option<String>,
pub has_supported: bool,
pub interests: Option<String>,
pub join_date: String,
pub kudosu: Kudosu,
pub location: Option<String>,
pub max_blocks: u64,
pub max_friends: u64,
pub occupation: Option<String>,
pub playmode: String,
pub playstyle: Option<Vec<String>>,
pub post_count: u64,
pub profile_order: Vec<String>,
pub skype: Option<String>,
pub title: Option<String>,
pub twitter: Option<String>,
pub website: Option<String>,
pub country: Country,
pub cover: Cover,
pub is_admin: Option<bool>,
pub is_bng: Option<bool>,
pub is_full_bn: Option<bool>,
pub is_gmt: Option<bool>,
pub is_limited_bn: Option<bool>,
pub is_moderator: Option<bool>,
pub is_nat: Option<bool>,
pub is_restricted: Option<bool>,
pub is_silenced: Option<bool>,
pub statistics: UserStatistics,
pub rank_history: Option<RankHistory>,
pub user_achivements: Option<Vec<UserAchivement>>,
pub beatmap_playcounts_count: u64,
pub favourite_beatmapset_count: u64,
pub follower_count: u64,
pub graveyard_beatmapset_count: u64,
pub monthly_playcounts: Option<Vec<MonthlyPlayCount>>,
}
#[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<String>,
pub pm_friends_only: bool,
pub profile_colour: Option<String>,
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<u32>,
}
#[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<CompactUser>,
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<u64>,
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<u64>,
pub country: Option<u64>,
}
#[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<String>,
url: String,
#[serde(default)]
#[serde(deserialize_with = "parse_to_int")]
id: Option<u64>,
}
fn parse_to_int<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
let stri: Option<String> = Option::deserialize(deserializer)?;
if let Some(s) = stri {
match s.parse::<u64>() {
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<Box<User>, Error>;
async fn search_user<'a>(&'a mut self, name: String) -> Result<Box<SearchResult>, Error>;
}