diff --git a/Cargo.toml b/Cargo.toml index 0006173..cd5b660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "tribufu" version = "0.0.4" description = "Tribufu SDK" -repository = "https://github.com/Tribufu/SDK-Rust" +repository = "https://github.com/Tribufu/TribufuRust" authors = ["Tribufu "] license = "Apache-2.0" readme = "README.md" @@ -17,6 +17,8 @@ crate-type = ["rlib"] path = "src/lib.rs" [dependencies] +base64 = "0.21.5" +mintaka-error = { version = "0.0.1" } alnilam-consts = { version = "0.0.4" } anyhow = "1.0.75" chrono = { version = "0.4.22", features = ["serde", "rustc-serialize"] } diff --git a/examples/api.rs b/examples/api.rs new file mode 100644 index 0000000..2a5c7f1 --- /dev/null +++ b/examples/api.rs @@ -0,0 +1,9 @@ +// Copyright (c) Tribufu. All Rights Reserved + +use tribufu::*; + +#[tokio::main] +async fn main() { + let mut api = TribufuApi::default(); + api.use_anonymous(); +} diff --git a/examples/client.rs b/examples/client.rs deleted file mode 100644 index 152db3b..0000000 --- a/examples/client.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Tribufu. All Rights Reserved - -use tribufu::*; - -#[tokio::main] -async fn main() { - match TribufuClient::new(0, "client_secret") { - Ok(client) => println!("client_id: {}", client.id()), - Err(e) => println!("error: {:?}", e), - } -} diff --git a/examples/token.rs b/examples/token.rs index b06bd6d..9f4a223 100644 --- a/examples/token.rs +++ b/examples/token.rs @@ -1,23 +1,8 @@ // Copyright (c) Tribufu. All Rights Reserved -use dotenv::dotenv; -use std::env; use tribufu::*; #[tokio::main] async fn main() { - dotenv().ok(); - - let client_id = env::var("CLIENT_ID").unwrap().parse::().unwrap(); - let client_secret = env::var("CLIENT_SECRET").unwrap(); - - let mut client = TribufuClient::new(client_id, client_secret).unwrap(); - - client.get_token(None).await.unwrap(); - - let games = client.get_games().await.unwrap(); - - games.iter().for_each(|game| { - println!("{}", game.name); - }); + let mut api = TribufuApi::from_env(); } diff --git a/src/actix/Cargo.toml b/src/actix/Cargo.toml new file mode 100644 index 0000000..238cd94 --- /dev/null +++ b/src/actix/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tribufu-actix" +version = "0.0.4" +description = "Tribufu Actix Extension" +repository = "https://github.com/Tribufu/TribufuRust" +authors = ["Tribufu "] +license = "Apache-2.0" +readme = "README.md" +edition = "2021" +publish = true + +[lib] +name = "tribufu_actix" +crate-type = ["rlib"] +path = "lib.rs" + +[dependencies] +actix-web = { version = "4", features = ["rustls"] } diff --git a/src/actix/lib.rs b/src/actix/lib.rs new file mode 100644 index 0000000..ae6c045 --- /dev/null +++ b/src/actix/lib.rs @@ -0,0 +1,29 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +/* +use actix_web::HttpRequest; + +pub trait TribufuApiActixExtension { + fn use_anonymous(req: &HttpRequest) -> Self; +} + +impl TribufuApi { + pub fn from_actix(req: &HttpRequest) -> Self { + let mut api = Self::default(); + + if let Some(api_key) = req.headers().get("X-Tribufu-Api-Key") { + api.use_api_key(api_key.to_str().unwrap().to_string()); + } + + if let Some(authorization) = req.headers().get("Authorization") { + let authorization = authorization.to_str().unwrap(); + + if authorization.starts_with("Bearer ") { + api.use_token(authorization[7..].to_string()); + } + } + + api + } +} +*/ diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..2397853 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,399 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +use crate::games::Game; +use crate::oauth2::{OAuth2GrantType, OAuth2TokenRequest, OAuth2TokenResponse}; +use crate::users::*; +use crate::VERSION; +use alnilam_consts::TARGET_TRIPLE; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine as _; +use chrono::{NaiveDateTime, Utc}; +use mintaka_error::{Error, Result}; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::Client; +use std::env; + +pub enum CredentialsType { + Anonymous, + ApiKey, + Basic, + Bearer, +} + +pub struct TribufuApi { + base_url: String, + credentials: Option, + credentials_kind: CredentialsType, + credentials_refreshed_at: Option, + credentials_expires_at: Option, + http: Client, +} + +impl Default for TribufuApi { + fn default() -> Self { + Self::new(CredentialsType::Anonymous, None) + } +} + +impl TribufuApi { + const BASE_URL: &'static str = "https://api.tribufu.com"; + + pub fn new(credentials_kind: CredentialsType, credentials: Option) -> Self { + let http = Client::builder() + .user_agent(Self::user_agent()) + .default_headers(Self::default_headers()) + .build() + .unwrap(); + + Self { + base_url: Self::BASE_URL.to_string(), + credentials, + credentials_kind, + credentials_refreshed_at: None, + credentials_expires_at: None, + http, + } + } + + pub fn debug_enabled(&self) -> bool { + return cfg!(debug_assertions); + } + + #[inline] + fn user_agent() -> String { + format!( + "Tribufu/{} (+https://api.tribufu.com; {})", + VERSION, TARGET_TRIPLE + ) + } + + #[inline] + fn default_headers() -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert("X-Tribufu-Language", HeaderValue::from_static("rust")); + headers.insert("X-Tribufu-Version", HeaderValue::from_static(VERSION)); + headers + } + + pub fn with_api_key(api_key: String) -> Self { + let mut api = Self::default(); + api.use_api_key(api_key); + api + } + + pub fn with_client(client_id: u64, client_secret: String) -> Self { + let mut api = Self::default(); + api.use_client(client_id, client_secret); + api + } + + pub fn with_token(token: String) -> Self { + let mut api = Self::default(); + api.use_token(token); + api + } + + pub fn from_env() -> Self { + let mut api = Self::default(); + api.use_env(); + api + } + + pub fn use_env(&mut self) { + #[cfg(debug_assertions)] + if let Ok(base_url) = env::var("TRIBUFU_API_URL") { + self.set_base_url(base_url); + } + + if let Ok(api_key) = env::var("TRIBUFU_API_KEY") { + self.use_api_key(api_key); + } + + let client_id = env::var("TRIBUFU_CLIENT_ID"); + let client_secret = env::var("TRIBUFU_CLIENT_SECRET"); + + if let (Ok(client_id), Ok(client_secret)) = (client_id, client_secret) { + self.use_client(client_id.parse().unwrap(), client_secret); + } + + if let Ok(token) = env::var("TRIBUFU_TOKEN") { + self.use_token(token); + } + } + + pub fn use_anonymous(&mut self) { + self.credentials_kind = CredentialsType::Anonymous; + self.credentials = None; + } + + pub fn use_api_key(&mut self, api_key: String) { + self.credentials_kind = CredentialsType::ApiKey; + self.credentials = Some(api_key); + } + + pub fn use_client(&mut self, client_id: u64, client_secret: String) { + let credentials_str = format!("{}:{}", client_id, client_secret); + self.credentials_kind = CredentialsType::Basic; + self.credentials = Some(BASE64.encode(credentials_str.as_bytes())); + } + + pub fn use_token(&mut self, token: String) { + self.credentials_kind = CredentialsType::Bearer; + self.credentials = Some(token); + } + + fn set_base_url(&mut self, base_url: String) { + self.base_url = base_url; + } + + #[inline] + fn headers(&self) -> HeaderMap { + let mut headers = Self::default_headers(); + + match self.credentials_kind { + CredentialsType::ApiKey => { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!( + "ApiKey {}", + self.credentials.as_ref().unwrap() + )) + .unwrap(), + ); + } + CredentialsType::Basic => { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Basic {}", self.credentials.as_ref().unwrap())) + .unwrap(), + ); + } + CredentialsType::Bearer => { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!( + "Bearer {}", + self.credentials.as_ref().unwrap() + )) + .unwrap(), + ); + } + _ => {} + } + + headers + } + + pub async fn get_token_with_code( + &mut self, + code: String, + client_id: u64, + client_secret: String, + ) -> Result { + self.get_oauth_token( + OAuth2GrantType::AuthorizationCode, + Some(code), + client_id, + client_secret, + None, + None, + ) + .await + } + + pub async fn get_token_from_password( + &mut self, + username: String, + password: String, + client_id: u64, + client_secret: String, + ) -> Result { + self.get_oauth_token( + OAuth2GrantType::Password, + Some(password), + client_id, + client_secret, + None, + Some(username), + ) + .await + } + + pub async fn get_token_from_passkey( + &mut self, + username: String, + passkey: String, + client_id: u64, + client_secret: String, + ) -> Result { + self.get_oauth_token( + OAuth2GrantType::Passkey, + Some(passkey), + client_id, + client_secret, + None, + Some(username), + ) + .await + } + + pub async fn refresh_token( + &mut self, + refresh_token: String, + client_id: u64, + client_secret: String, + ) -> Result { + self.get_oauth_token( + OAuth2GrantType::RefreshToken, + Some(refresh_token), + client_id, + client_secret, + None, + None, + ) + .await + } + + pub async fn get_client_token( + &mut self, + client_id: u64, + client_secret: String, + ) -> Result { + self.get_oauth_token( + OAuth2GrantType::ClientCredentials, + None, + client_id, + client_secret, + None, + None, + ) + .await + } + + pub async fn get_server_token( + &mut self, + server_id: u64, + client_id: u64, + client_secret: String, + ) -> Result { + self.get_oauth_token( + OAuth2GrantType::ClientCredentials, + None, + client_id, + client_secret, + Some("server_id".to_string()), + Some(server_id.to_string()), + ) + .await + } + + async fn get_oauth_token( + &mut self, + grant_type: OAuth2GrantType, + grant_value: Option, + client_id: u64, + client_secret: String, + subject_key: Option, + subject_value: Option, + ) -> Result { + let code = if grant_type == OAuth2GrantType::AuthorizationCode { + grant_value.clone() + } else { + None + }; + + let refresh_token = if grant_type == OAuth2GrantType::RefreshToken { + grant_value.clone() + } else { + None + }; + + let mut require_username = false; + + let password = if grant_type == OAuth2GrantType::Password { + require_username = true; + grant_value.clone() + } else { + None + }; + + let passkey = if grant_type == OAuth2GrantType::Passkey { + require_username = true; + grant_value.clone() + } else { + None + }; + + let username = if require_username && subject_value.is_some() { + subject_value.clone() + } else { + None + }; + + let request_body = OAuth2TokenRequest { + grant_type, + code, + refresh_token, + username, + password, + passkey, + client_id: Some(client_id.to_string()), + client_secret: Some(client_secret.clone()), + redirect_uri: None, + }; + + let params = if subject_key.is_some() && subject_value.is_some() { + format!("?{}={}", subject_key.unwrap(), subject_value.unwrap()) + } else { + "".to_string() + }; + + let url = format!("{}/v1/oauth2/token{}", self.base_url, params); + let headers = self.headers(); + let response = self + .http + .post(url) + .headers(headers) + .form(&request_body) + .send() + .await?; + + if response.status() != 200 { + return Err(Error::msg(format!( + "Failed to get token: {}", + response.status() + ))); + } + + let response_body: OAuth2TokenResponse = response.json().await?; + + self.use_token(response_body.clone().access_token); + + Ok(response_body) + } + + pub async fn get_user_info(&self) -> Result { + let url = format!("{}/v1/oauth2/userinfo", self.base_url); + let headers = self.headers(); + let response = self.http.get(url).headers(headers).send().await?; + + Ok(response.json().await?) + } + + pub async fn get_games(&self, page: Option) -> Result> { + let page = page.unwrap_or(1); + let url = format!("{}/v1/packages?page={}", self.base_url, page); + let headers = self.headers(); + let response = self.http.get(url).headers(headers).send().await?; + + Ok(response.json().await?) + } + + pub async fn get_game(&self, id: u64) -> Result { + let url = format!("{}/v1/packages/{}", self.base_url, id); + let headers = self.headers(); + let response = self.http.get(url).headers(headers).send().await?; + + Ok(response.json().await?) + } +} diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 9dba9f2..0000000 --- a/src/client.rs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Tribufu. All Rights Reserved. - -use crate::games::Game; -use crate::oauth2::*; -use crate::VERSION; -use alnilam_consts::TARGET_TRIPLE; -use anyhow::{Error, Result}; -use reqwest::header; -use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::Client; - -#[derive(Clone)] -pub struct TribufuClient { - client_id: u64, - client_secret: String, - token: Option, -} - -impl TribufuClient { - //const BASE_URL: &'static str = "https://api.tribufu.com"; - const BASE_URL: &'static str = "http://localhost:5000"; - - pub fn new(id: u64, secret: impl Into) -> Result { - Ok(TribufuClient { - client_id: id, - client_secret: secret.into(), - token: None, - }) - } - - #[inline] - pub fn id(&self) -> u64 { - self.client_id - } - - #[inline] - pub fn user_agent() -> String { - format!( - "Tribufu/{} (+https://api.tribufu.com; {})", - VERSION, TARGET_TRIPLE - ) - } - - #[inline] - fn default_headers() -> HeaderMap { - let mut headers = HeaderMap::new(); - headers.insert("X-Tribufu-Language", HeaderValue::from_static("rust")); - headers.insert("X-Tribufu-Version", HeaderValue::from_static(VERSION)); - headers - } - - fn http_client(&self) -> Result { - let user_agent = Self::user_agent(); - let mut headers = Self::default_headers(); - - if let Some(token) = &self.token { - headers.insert( - header::AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", token.access_token))?, - ); - } - - let http = Client::builder() - .user_agent(user_agent) - .default_headers(headers) - .build()?; - - Ok(http) - } - - pub async fn get_token(&mut self, server_id: Option) -> Result<()> { - let server_id = if let Some(server_id) = server_id { - Some(server_id.to_string()) - } else { - None - }; - - let body = OAuth2TokenRequest { - grant_type: OAuth2GrantType::ClientCredentials, - code: None, - refresh_token: None, - username: None, - password: None, - client_id: Some(self.client_id.to_string()), - client_secret: Some(self.client_secret.clone()), - redirect_uri: None, - server_id, - }; - - let url = format!("{}/v1/oauth2/token", Self::BASE_URL); - let response = self.http_client()?.post(url).form(&body).send().await?; - - if response.status() != 200 { - return Err(Error::msg(format!( - "Failed to get token: {}", - response.status() - ))); - } - - self.token = Some(response.json().await?); - - Ok(()) - } - - pub async fn refresh_token_token(&mut self) -> Result<()> { - let token = if let Some(token) = &self.token { - token - } else { - return Err(Error::msg( - format!("Failed to refresh: self.token == None",), - )); - }; - - if token.refresh_token.is_none() { - return Err(Error::msg(format!( - "Failed to refresh: self.token.refresh_token == None", - ))); - } - - let body = OAuth2TokenRequest { - grant_type: OAuth2GrantType::RefreshToken, - code: None, - refresh_token: token.refresh_token.clone(), - username: None, - password: None, - client_id: Some(self.client_id.to_string()), - client_secret: Some(self.client_secret.clone()), - redirect_uri: None, - server_id: None, - }; - - let url = format!("{}/v1/oauth2/token", Self::BASE_URL); - let response = self.http_client()?.post(url).form(&body).send().await?; - - if response.status() != 200 { - return Err(Error::msg(format!( - "Failed to get token: {}", - response.status() - ))); - } - - self.token = Some(response.json().await?); - - Ok(()) - } - - pub async fn get_games(&self) -> Result> { - let url = format!("{}/v1/packages", Self::BASE_URL); - let response = self.http_client()?.get(url).send().await?; - - Ok(response.json().await?) - } - - pub async fn get_game(&self, id: u64) -> Result { - let url = format!("{}/v1/packages/{}", Self::BASE_URL, id); - let response = self.http_client()?.get(url).send().await?; - - Ok(response.json().await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_client() { - let client = TribufuClient::new(0, "client_secret").unwrap(); - assert_eq!(client.id(), 0); - } -} diff --git a/src/lib.rs b/src/lib.rs index 040dee1..18f6a41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,9 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub mod client; +pub mod api; pub mod games; pub mod oauth2; -pub mod token; +pub mod users; -pub use client::*; +pub use api::*; diff --git a/src/oauth2.rs b/src/oauth2.rs index 07b2067..9691e73 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -2,38 +2,39 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum OAuth2ResponseType { Code, Token, } -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum OAuth2ClientType { Confidential, Public, } -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum OAuth2TokenHintType { AccessToken, RefreshToken, } -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum OAuth2GrantType { AuthorizationCode, ClientCredentials, DeviceCode, + Passkey, Password, RefreshToken, } -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum OAuth2AuthorizeError { AccessDenied, @@ -45,7 +46,7 @@ pub enum OAuth2AuthorizeError { UnsupportedResponseType, } -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum OAuth2TokenType { Bearer, @@ -86,34 +87,47 @@ pub struct OAuth2ErrorResponse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OAuth2TokenRequest { pub grant_type: OAuth2GrantType, + #[serde(skip_serializing_if = "Option::is_none")] pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub refresh_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub passkey: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub client_secret: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub redirect_uri: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub server_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OAuth2TokenResponse { pub token_type: OAuth2TokenType, + pub access_token: String, + #[serde(skip_serializing_if = "Option::is_none")] pub refresh_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub state: Option, + pub expires_in: u64, } diff --git a/src/token.rs b/src/token.rs deleted file mode 100644 index 208bb0e..0000000 --- a/src/token.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Tribufu. All Rights Reserved. - -use crate::oauth2::OAuth2TokenResponse; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Credentials { - ApiKey(String), - Token(OAuth2TokenResponse), -} diff --git a/src/users.rs b/src/users.rs new file mode 100644 index 0000000..599bf66 --- /dev/null +++ b/src/users.rs @@ -0,0 +1,41 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +use chrono::{NaiveDate, NaiveDateTime}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UserType { + User = 0, + Bot = 1, + Org = 2, +} + +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + #[serde_as(as = "DisplayFromStr")] + pub id: u64, + pub uuid: String, + pub name: String, + pub display_name: String, + #[serde(rename = "type")] + pub kind: UserType, + pub public_flags: u64, + pub verified: bool, + pub level: u32, + pub experience: f64, + pub public_birthday: bool, + pub birthday: Option, + pub points: f64, + pub location: Option, + pub photo_url: Option, + pub banner_url: Option, + pub last_online: Option, + pub biography: Option, + pub view_count: u32, + pub created: NaiveDateTime, + pub updated: Option, +}