diff --git a/.editorconfig b/.editorconfig index 88df879..b903496 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[*.env*] +insert_final_newline = false diff --git a/.env.example b/.env.example index 46cda10..132e464 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ CLIENT_ID= CLIENT_SECRET= +TRIBUFU_API_URL=https://api.tribufu.com \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 292870f..0006173 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,12 @@ path = "src/lib.rs" [dependencies] alnilam-consts = { version = "0.0.4" } anyhow = "1.0.75" +chrono = { version = "0.4.22", features = ["serde", "rustc-serialize"] } derive_more = "0.99.17" reqwest = { version = "0.11.18", features = ["json", "stream"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } -chrono = { version = "0.4.22", features = ["serde", "rustc-serialize"] } +serde_with = "3.4.0" [dev-dependencies] dotenv = "0.15.0" diff --git a/examples/token.rs b/examples/token.rs index 7e230e4..b06bd6d 100644 --- a/examples/token.rs +++ b/examples/token.rs @@ -11,8 +11,13 @@ async fn main() { let client_id = env::var("CLIENT_ID").unwrap().parse::().unwrap(); let client_secret = env::var("CLIENT_SECRET").unwrap(); - let client = TribufuClient::new(client_id, client_secret).unwrap(); - let token = client.get_token().await.unwrap(); + let mut client = TribufuClient::new(client_id, client_secret).unwrap(); - println!("{:?}", token) + client.get_token(None).await.unwrap(); + + let games = client.get_games().await.unwrap(); + + games.iter().for_each(|game| { + println!("{}", game.name); + }); } diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..9dba9f2 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,171 @@ +// 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/games.rs b/src/games.rs new file mode 100644 index 0000000..e34c3ad --- /dev/null +++ b/src/games.rs @@ -0,0 +1,29 @@ +// Copyright (c) Tribufu. All Rights Reserved. + +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Game { + #[serde_as(as = "DisplayFromStr")] + pub id: u64, + pub name: String, + pub description: Option, + pub icon_url: Option, + pub banner_url: Option, + pub capsule_image_url: Option, + pub library_image_url: Option, + pub slug: Option, + pub game_port: Option, + pub query_port: Option, + pub rcon_port: Option, + pub steam_app_id: Option, + pub steam_server_app_id: Option, + pub rust_gamedig_id: Option, + pub node_gamedig_id: Option, + pub server_connect_url: Option, + pub created: NaiveDateTime, + pub updated: Option, +} diff --git a/src/lib.rs b/src/lib.rs index 60724b0..040dee1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,95 +2,11 @@ #![allow(dead_code)] -use alnilam_consts::TARGET_TRIPLE; -use anyhow::{Error, Result}; -use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::Client; - pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub mod client; +pub mod games; pub mod oauth2; pub mod token; -#[derive(Clone)] -pub struct TribufuClient { - client_id: u64, - client_secret: String, - http: Client, -} - -impl TribufuClient { - const BASE_URL: &'static str = "http://localhost:5000"; - - pub fn new(id: u64, secret: impl Into) -> Result { - let user_agent = format!( - "Tribufu/{} (+https://api.tribufu.com; {})", - VERSION, TARGET_TRIPLE - ); - - let mut headers = HeaderMap::new(); - headers.insert("X-Tribufu-Language", HeaderValue::from_static("rust")); - headers.insert("X-Tribufu-Version", HeaderValue::from_static(VERSION)); - - let http = Client::builder() - .default_headers(headers) - .user_agent(user_agent) - .build()?; - - Ok(TribufuClient { - client_id: id, - client_secret: secret.into(), - http, - }) - } - - pub fn id(&self) -> u64 { - self.client_id - } - - pub async fn get_token(&self) -> Result { - let body = oauth2::OAuth2TokenRequest { - grant_type: oauth2::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, - }; - - let response = match self - .http - .post(format!("{}/v1/oauth2/token", Self::BASE_URL)) - .form(&body) - .send() - .await - { - Ok(r) => r, - Err(e) => return Err(e.into()), - }; - - if response.status() != 200 { - return Err(Error::msg(format!( - "Failed to get token: {}", - response.status() - ))); - } - - let token = response.json::().await?; - - Ok(token) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_client() { - let client = TribufuClient::new(0, "client_secret").unwrap(); - assert_eq!(client.id(), 0); - } -} +pub use client::*; diff --git a/src/oauth2.rs b/src/oauth2.rs index a869a5a..07b2067 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -86,29 +86,34 @@ pub struct OAuth2ErrorResponse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OAuth2TokenRequest { pub grant_type: OAuth2GrantType, - pub client_id: Option, - pub client_secret: Option, - pub redirect_uri: Option, + #[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 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, - - pub refresh_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 index c3adf4e..208bb0e 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,20 +1,11 @@ // Copyright (c) Tribufu. All Rights Reserved. +use crate::oauth2::OAuth2TokenResponse; use serde::{Deserialize, Serialize}; -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum TokenType { - User, - Bot, - Client, - Server, -} - -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AuthorizationType { - ApiKey, - Basic, - Bearer, +pub enum Credentials { + ApiKey(String), + Token(OAuth2TokenResponse), }