From f42a1cdebebb28304f89c23cdb5da9d3a4468348 Mon Sep 17 00:00:00 2001
From: Guilherme Werner <guilherme.werner@tribufu.com>
Date: Sun, 24 Dec 2023 08:22:52 -0300
Subject: [PATCH] Add games routes

---
 .editorconfig     |   3 +
 .env.example      |   1 +
 Cargo.toml        |   3 +-
 examples/token.rs |  11 ++-
 src/client.rs     | 171 ++++++++++++++++++++++++++++++++++++++++++++++
 src/games.rs      |  29 ++++++++
 src/lib.rs        |  90 +-----------------------
 src/oauth2.rs     |  23 ++++---
 src/token.rs      |  19 ++----
 9 files changed, 236 insertions(+), 114 deletions(-)
 create mode 100644 src/client.rs
 create mode 100644 src/games.rs

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::<u64>().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<OAuth2TokenResponse>,
+}
+
+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<String>) -> Result<TribufuClient> {
+        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<Client> {
+        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<u64>) -> 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<Vec<Game>> {
+        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<Game> {
+        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<String>,
+    pub icon_url: Option<String>,
+    pub banner_url: Option<String>,
+    pub capsule_image_url: Option<String>,
+    pub library_image_url: Option<String>,
+    pub slug: Option<String>,
+    pub game_port: Option<u16>,
+    pub query_port: Option<u16>,
+    pub rcon_port: Option<u16>,
+    pub steam_app_id: Option<u32>,
+    pub steam_server_app_id: Option<u32>,
+    pub rust_gamedig_id: Option<String>,
+    pub node_gamedig_id: Option<String>,
+    pub server_connect_url: Option<String>,
+    pub created: NaiveDateTime,
+    pub updated: Option<NaiveDateTime>,
+}
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<String>) -> Result<TribufuClient> {
-        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<oauth2::OAuth2TokenResponse> {
-        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::<oauth2::OAuth2TokenResponse>().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<String>,
-    pub client_secret: Option<String>,
-    pub redirect_uri: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub code: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub refresh_token: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub username: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub password: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub client_id: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub client_secret: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub redirect_uri: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub server_id: Option<String>,
 }
 
 #[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<String>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub scope: Option<String>,
-
     #[serde(skip_serializing_if = "Option::is_none")]
     pub state: Option<String>,
-
     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),
 }