diff --git a/Cargo.toml b/Cargo.toml index b3592e6..f3ad9b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfj" -version = "0.6.1" +version = "0.7.0" authors = ["NGnius (Graham) "] edition = "2018" description = "An unofficial collection of APIs used in FreeJam games and mods" @@ -21,8 +21,6 @@ serde_json = "^1" reqwest = { version = "^0.11", features = ["json"], optional = true} url = "^2.2" ureq = { version = "^2", features = ["json"], optional = true} -cookie_store = { version = "0.16", optional = true} -cookie = { version = "0.16", optional = true} async-trait = { version = "0.1", optional = true } base64 = "^0.13" num_enum = "^0.5" @@ -46,4 +44,4 @@ robocraft = ["reqwest", "ureq"] cardlife = ["reqwest"] techblox = ["chrono", "highhash", "half", "libfj_parsable_macro_derive"] convert = ["obj", "genmesh", "cgmath"] -robocraft2 = ["reqwest", "reqwest/cookies", "async-trait"] +robocraft2 = ["reqwest", "async-trait", "chrono"] diff --git a/src/robocraft2/factory.rs b/src/robocraft2/factory.rs index 0609fd3..a835f8b 100644 --- a/src/robocraft2/factory.rs +++ b/src/robocraft2/factory.rs @@ -1,13 +1,49 @@ use std::sync::Mutex; -use reqwest::{Client, Error}; +use reqwest::{Client, Error as ReqwestError, Response}; use url::{Url}; -use crate::robocraft2::{SearchPayload, SearchResponse, ITokenProvider}; +use crate::robocraft2::{ITokenProvider, ErrorPayload}; +use crate::robocraft2::{SearchPayload, SearchResponse, CreateRobotPayload, CreateRobotResponse, FactoryInfoResponse, PublishRobotPayload, PublishRobotResponse, MyRobotsResponse, GetRobotResponse}; /// Community Factory Robot 2 root URL pub const FACTORY_DOMAIN: &str = "https://factory.production.robocraft2.com"; +#[derive(Debug)] +pub enum FactoryError { + Protocol(ReqwestError), + Response(ErrorPayload), + ResponseCode(ReqwestError, u16) +} + +impl std::fmt::Display for FactoryError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + Self::Protocol(p) => if let Some(status) = p.status() { + write!(f, "HTTP Error {}: {}", status, p) + } else { + write!(f, "HTTP Error: {}", p) + } + Self::Response(r) => write!(f, "Factory Error #{}: {}", r.error, r.error_message), + Self::ResponseCode(p, s) => write!(f, "HTTP Error {}: {}", s, p) + } + } +} + +impl std::error::Error for FactoryError {} + +async fn handle_json_response serde::Deserialize<'a>>(response: Response) -> Result { + let status_code: u16 = response.status().into(); + if status_code > 199 && status_code < 300 { + Ok(response.json::().await.map_err(FactoryError::Protocol)?) + } else { + match response.json::().await { + Ok(err) => Err(FactoryError::Response(err)), + Err(e) => Err(FactoryError::ResponseCode(e, status_code)) + } + } +} + /// CRF API implementation pub struct FactoryAPI { client: Client, @@ -32,13 +68,12 @@ impl FactoryAPI { } /// Retrieve CRF robots on the main page. - /// - /// For searching, use `list_builder()` instead. - pub async fn list(&self) -> Result { + pub async fn list(&self) -> Result { self.search(SearchPayload::default()).await } - pub async fn search(&self, params: SearchPayload) -> Result { + /// Search for robots on the CRF which meet the provided parameters + pub async fn search(&self, params: SearchPayload) -> Result { let mut url = Url::parse(FACTORY_DOMAIN) .unwrap() .join("/v1/foundry/search") @@ -87,11 +122,136 @@ impl FactoryAPI { } url.query_pairs_mut().append_pair("sortBy", ¶ms.sort_by); url.query_pairs_mut().append_pair("orderBy", ¶ms.order_by); - let mut request_builder = self.client.get(url); - if let Ok(token) = self.token.lock().unwrap().token().await { - request_builder = request_builder.header("Authorization", "Bearer ".to_owned() + &token); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.get(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send().await + .map_err(FactoryError::Protocol)?; + handle_json_response::(result).await + } + + pub async fn create_robot(&self, robot: CreateRobotPayload) -> Result { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join("/v1/foundry/garage") + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.post(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .header("Content-Type", "application/json") + .json(&robot) + .send().await + .map_err(FactoryError::Protocol)?; + handle_json_response::(result).await + } + + pub async fn publish_robot(&self, robot: PublishRobotPayload, id: String) -> Result { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join(&format!("/v1/foundry/vehicles/{}/publish", id)) + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.post(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .header("Content-Type", "application/json") + .json(&robot) + .send().await + .map_err(FactoryError::Protocol)?; + handle_json_response(result).await + } + + pub async fn unpublish_bot(&self, id: String) -> Result<(), FactoryError> { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join(&format!("/v1/foundry/vehicles/{}/unpublish", id)) + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.post(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send().await + .map_err(FactoryError::Protocol)?; + let status_code = result.status().as_u16(); + if status_code > 199 && status_code < 300 { + Ok(()) + } else { + match result.json::().await { + Ok(err) => Err(FactoryError::Response(err)), + Err(e) => Err(FactoryError::ResponseCode(e, status_code)) + } + } + } + + pub async fn delete_robot(&self, id: String) -> Result<(), FactoryError> { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join(&format!("/v1/foundry/vehicles/{}", id)) + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.delete(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .header("Content-Type", "application/json") + .send().await + .map_err(FactoryError::Protocol)?; + let status_code = result.status().as_u16(); + if status_code > 199 && status_code < 300 { + Ok(()) + } else { + match result.json::().await { + Ok(err) => Err(FactoryError::Response(err)), + Err(e) => Err(FactoryError::ResponseCode(e, status_code)) + } } - let result = request_builder.send().await?; - result.json::().await + } + + pub async fn factory_info(&self) -> Result { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join("/v1/foundry/info") + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.get(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send().await + .map_err(FactoryError::Protocol)?; + handle_json_response::(result).await + } + + pub async fn my_robots(&self) -> Result { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join("/v1/foundry/garage") + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.get(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send().await + .map_err(FactoryError::Protocol)?; + handle_json_response::(result).await + } + + pub async fn my_published_robots(&self) -> Result { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join("/v1/foundry/published") + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.get(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send().await + .map_err(FactoryError::Protocol)?; + handle_json_response::(result).await + } + + pub async fn get(&self, id: String) -> Result { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join(&format!("/v1/foundry/vehicles/{}", id)) + .unwrap(); + let token = self.token.lock().unwrap().token().await.map_err(FactoryError::Protocol)?; + let result = self.client.get(url) + .header("Authorization", "Bearer ".to_owned() + &token) + .send().await + .map_err(FactoryError::Protocol)?; + handle_json_response::(result).await } } diff --git a/src/robocraft2/factory_json.rs b/src/robocraft2/factory_json.rs index 04518c9..4669801 100644 --- a/src/robocraft2/factory_json.rs +++ b/src/robocraft2/factory_json.rs @@ -1,5 +1,13 @@ use serde::{Deserialize, Serialize}; +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ErrorPayload { + #[serde(rename = "error")] + pub error: isize, + #[serde(rename = "errorMessage")] + pub error_message: String, +} + // search endpoint #[derive(Deserialize, Serialize, Clone)] @@ -119,7 +127,7 @@ pub struct RobotInfo { impl std::string::ToString for RobotInfo { fn to_string(&self) -> String { - format!("{} by {} ({})", &self.name, &self.creator_name, &self.id) + format!("{} ({}) by {} ({})", &self.name, &self.id, &self.creator_name, &self.creator_id) } } @@ -130,3 +138,109 @@ pub struct RobotPrice { #[serde(rename = "amount")] pub amount: isize, } + +// create robot endpoint + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct CreateRobotPayload { + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "data")] + pub data: String, // base64 + #[serde(rename = "image")] + pub image: String, // base64?? + #[serde(rename = "baseCpu")] + pub base_cpu: isize, + #[serde(rename = "weaponCpu")] + pub weapon_cpu: isize, + #[serde(rename = "cosmeticCpu")] + pub cosmetic_cpu: isize, + #[serde(rename = "clusterCount")] + pub cluster_count: isize, + #[serde(rename = "blockCounts")] + pub block_counts: std::collections::HashMap, + #[serde(rename = "materialsUsed")] + pub materials_used: std::collections::HashSet, + #[serde(rename = "minimumOffsetX")] + pub minimum_offset_x: f64, + #[serde(rename = "minimumOffsetY")] + pub minimum_offset_y: f64, + #[serde(rename = "minimumOffsetZ")] + pub minimum_offset_z: f64, + #[serde(rename = "maximumOffsetX")] + pub maximum_offset_x: f64, + #[serde(rename = "maximumOffsetY")] + pub maximum_offset_y: f64, + #[serde(rename = "maximumOffsetZ")] + pub maximum_offset_z: f64, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct CreateRobotResponse { + #[serde(rename = "header")] + pub header: RobotInfo, +} + +// factory info endpoint + +// (no payload -- this is a GET request) + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct FactoryInfoResponse { + #[serde(rename = "robotCount")] + pub robot_count: isize, + #[serde(rename = "robotLimit")] + pub robot_limit: isize, + #[serde(rename = "publishedRobotCount")] + pub published_robot_count: isize, + #[serde(rename = "publishedRobotLimit")] + pub published_robot_limit: isize, +} + +// publish robot endpoint + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct PublishRobotPayload { + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "description")] + pub description: String, + #[serde(rename = "techPoints")] + pub techpoints: isize, + #[serde(rename = "bloxCoin")] + pub bloxcoin: isize, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct PublishRobotResponse { + #[serde(rename = "header")] + pub header: RobotInfo, + #[serde(rename = "data")] + pub data: String, // base64 +} + +// get my robots endpoint + +// (no payload -- this is a GET request) + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct MyRobotsResponse { + #[serde(rename = "vehicles")] + pub vehicles: Vec, +} + +// get robot endpoint + +// (no payload -- this is a GET request) + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct GetRobotResponse { + #[serde(rename = "header")] + pub header: RobotInfo, + #[serde(rename = "data")] + pub data: String, // base64 + #[serde(rename = "description")] + pub description: String, + #[serde(rename = "created")] + pub created: String, // date +} diff --git a/src/robocraft2/mod.rs b/src/robocraft2/mod.rs index 9b12e67..cc22ba3 100644 --- a/src/robocraft2/mod.rs +++ b/src/robocraft2/mod.rs @@ -2,10 +2,10 @@ //! Subject to change and breakages as RC2 is still in an early development stage. mod factory; -pub use factory::FactoryAPI; +pub use factory::{FactoryAPI, FactoryError}; mod factory_json; -pub use factory_json::{SearchPayload, SearchResponse, SearchResponseItem, RobotInfo, RobotPrice}; +pub use factory_json::{ErrorPayload, SearchPayload, SearchResponse, SearchResponseItem, RobotInfo, RobotPrice, CreateRobotPayload, CreateRobotResponse, FactoryInfoResponse, PublishRobotPayload, PublishRobotResponse, MyRobotsResponse, GetRobotResponse}; mod portal; pub use self::portal::{PortalTokenProvider, AccountInfo, PortalCheckResponse, ITokenProvider}; diff --git a/src/robocraft2/portal.rs b/src/robocraft2/portal.rs index 555cec2..d311584 100644 --- a/src/robocraft2/portal.rs +++ b/src/robocraft2/portal.rs @@ -4,12 +4,13 @@ use reqwest::{Client, Error}; //use cookie_store::CookieStore; //use url::{Url}; use serde_json::from_slice; +use chrono::{DateTime, naive::NaiveDateTime, Utc}; /// Token generator for authenticated API endpoints #[async_trait::async_trait] pub trait ITokenProvider { /// Retrieve the token to use - async fn token(&mut self) -> Result; + async fn token(&mut self) -> Result; } /// Token provider for an existing Freejam account, authenticated through the web browser portal. @@ -22,6 +23,8 @@ pub struct PortalTokenProvider { jwt: PortalCheckResponse, /// Ureq HTTP client client: Client, + /// target game + target: String, } impl PortalTokenProvider { @@ -34,7 +37,7 @@ impl PortalTokenProvider { pub async fn target(value: String) -> Result { let client = Client::new(); let payload = PortalStartPayload { - target: value, + target: value.clone(), }; let start_response = client.post("https://account.freejamgames.com/api/authenticate/portal/start") .header("Content-Type", "application/json") @@ -62,7 +65,7 @@ impl PortalTokenProvider { let check_res = check_response.json::().await?; // login with token we just got - Self::login_internal(check_res, client).await + Self::login_internal(check_res, client, value).await } pub async fn with_email(email: &str, password: &str) -> Result { @@ -96,7 +99,7 @@ impl PortalTokenProvider { /// Automatically validate portal async fn auto_portal(client: Client, value: String, token: String) -> Result { let payload = PortalStartPayload { - target: value, + target: value.clone(), }; let start_response = client.post("https://account.freejamgames.com/api/authenticate/portal/start") .header("Content-Type", "application/json") @@ -121,10 +124,20 @@ impl PortalTokenProvider { let check_res = check_response.json::().await?; // login with token we just got - Self::login_internal(check_res, client).await + Self::login_internal(check_res, client, value).await } - async fn login_internal(token_data: PortalCheckResponse, client: Client) -> Result { + async fn login_internal(token_data: PortalCheckResponse, client: Client, target: String) -> Result { + let progress_res = Self::login_step(&token_data, &client).await?; + Ok(Self { + token: progress_res, + jwt: token_data, + client: client, + target: target, + }) + } + + async fn login_step(token_data: &PortalCheckResponse, client: &Client) -> Result { let payload = ProgressionLoginPayload { token: token_data.token.clone(), }; @@ -132,17 +145,12 @@ impl PortalTokenProvider { .header("Content-Type", "application/json") .json(&payload) .send().await?; - let progress_res = progress_response.json::().await?; - Ok(Self { - token: progress_res, - jwt: token_data, - client: client, - }) + progress_response.json::().await } /// Login using the portal token data from a previous portal authentication - pub async fn login(token_data: PortalCheckResponse) -> Result { - Self::login_internal(token_data, Client::new()).await + pub async fn login(token_data: PortalCheckResponse, target: String) -> Result { + Self::login_internal(token_data, Client::new(), target).await } pub fn get_account_info(&self) -> Result { @@ -156,13 +164,27 @@ impl PortalTokenProvider { #[async_trait::async_trait] impl ITokenProvider for PortalTokenProvider { - async fn token(&mut self) -> Result { - // TODO re-authenticate when expired - if let Some(token) = self.token.token.clone() { - Ok(token) - } else { - Err(()) + async fn token(&mut self) -> Result { + let decoded_jwt = self.jwt.decode_jwt_data(); + let expiry = DateTime::::from_utc(NaiveDateTime::from_timestamp(decoded_jwt.exp as i64, 0), Utc); + let now = Utc::now(); + if now >= expiry || self.token.token.is_none() { + // refresh token when expired + // TODO make sure refresh token isn't also expired + // (it would be a bit concerning if you decide to run libfj for 1+ month, though) + let payload = RefreshTokenPayload { + target: self.target.clone(), + refresh_token: self.jwt.refresh_token.clone(), + public_id: decoded_jwt.public_id, + }; + let refresh_response = self.client.post("https://account.freejamgames.com/api/authenticate/token/refresh") + .header("Content-Type", "application/json") + .json(&payload) + .send().await?; + self.jwt = refresh_response.json::().await?; + self.token = Self::login_step(&self.jwt, &self.client).await?; } + Ok(self.token.token.clone().unwrap()) } } @@ -249,6 +271,16 @@ pub(crate) struct ProgressionLoginResponse { pub server_token: Option, } +#[derive(Deserialize, Serialize, Clone, Debug)] +pub(crate) struct RefreshTokenPayload { + #[serde(rename = "Target")] + pub target: String, // "Techblox" + #[serde(rename = "RefreshToken")] + pub refresh_token: String, + #[serde(rename = "PublicId")] + pub public_id: String, +} + /// Robocraft2 account information. #[derive(Deserialize, Serialize, Clone)] pub struct AccountInfo { diff --git a/tests/robocraft_factory.rs b/tests/robocraft_factory.rs index df9aefc..b3c6012 100644 --- a/tests/robocraft_factory.rs +++ b/tests/robocraft_factory.rs @@ -31,24 +31,6 @@ async fn robocraft_factory_default_query() -> Result<(), ()> { Ok(()) } -#[cfg(feature = "robocraft2")] -#[tokio::test] -async fn robocraft2_factory_default_query() -> Result<(), ()> { - let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); - let result = api.list().await; - assert!(result.is_ok()); - let robo_info = result.unwrap(); - assert_ne!(robo_info.results.len(), 0); - for robot in &robo_info.results { - assert_ne!(robot.robot.name, ""); - assert_ne!(robot.robot.creator_id, ""); - assert_ne!(robot.robot.creator_id, ""); - assert_ne!(robot.robot.image, ""); - //println!("FactoryRobotListInfo.to_string() -> `{}`", robot.to_string()); - } - Ok(()) -} - #[cfg(feature = "robocraft")] fn builder() -> robocraft::FactorySearchBuilder { robocraft::FactoryAPI::new().list_builder() @@ -139,3 +121,178 @@ async fn robocraft_factory_robot_cubes() -> Result<(), ()> { assert_eq!(colour_str, bot_info.response.colour_data); Ok(()) } + +#[cfg(feature = "robocraft2")] +//#[tokio::test] +async fn robocraft2_factory_default_query() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + let result = api.list().await; + assert!(result.is_ok()); + let robo_info = unwrap_factory2(result); + assert_ne!(robo_info.results.len(), 0); + for robot in &robo_info.results { + assert_ne!(robot.robot.name, ""); + assert_ne!(robot.robot.creator_id, ""); + assert_ne!(robot.robot.creator_id, ""); + assert_ne!(robot.robot.image, ""); + //println!("RobotInfo.to_string() -> `{}`", robot.robot.to_string()); + println!("SearchResponseItem {}", serde_json::to_string_pretty(&robot).unwrap()); + } + Ok(()) +} + + +#[cfg(feature = "robocraft2")] +#[tokio::test] +async fn robocraft2_factory_info() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + let result = api.factory_info().await; + assert!(result.is_ok()); + let crf_info = unwrap_factory2(result); + println!("FactoryInfo {:?}", crf_info); + Ok(()) +} + +#[cfg(feature = "robocraft2")] +//#[tokio::test] +async fn robocraft2_factory_upload() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + + // copy default bot + let result = api.get("08dab2c9-7a72-4ec4-843c-154fe8768e91".to_owned()).await; + assert!(result.is_ok()); + let robot = unwrap_factory2(result); + + let result = api.create_robot( + robocraft2::CreateRobotPayload { + name: "API is easy".to_owned(), + data: robot.data, // base64 + image: "".to_owned(), // base64 + base_cpu: 42, + weapon_cpu: 1, + cosmetic_cpu: 6, + cluster_count: 1, + block_counts: vec![ + (42, 3), + (3, 6) + ].drain(..).collect(), + materials_used: vec![ + 8, + 4 + ].drain(..).collect(), + minimum_offset_x: 0.0, + minimum_offset_y: 0.0, + minimum_offset_z: 0.0, + maximum_offset_x: 0.0, + maximum_offset_y: 0.0, + maximum_offset_z: 0.0, + } + ).await; + //assert!(result.is_ok()); + let robot_info = unwrap_factory2(result); + println!("CreateRobotInfo {:?}", robot_info); + let result = api.publish_robot( + robocraft2::PublishRobotPayload { + name: "CRF API oh my".to_owned(), + description: "There once was a person named NGnius, who simply wasn't that bright.\nBut he had a misleading name, and wasn't not quite unhated\nSo he thought it alright to put up a fight\nand get banned for reverse engineering".to_owned(), + techpoints: -42, + bloxcoin: 123 + }, robot_info.header.id.clone()).await; + //assert!(result.is_ok()); + let _publish_info = unwrap_factory2(result); + + // clean up + let result = api.unpublish_bot(robot_info.header.id.clone()).await; + //assert!(result.is_ok()); + let _robot = unwrap_factory2(result); + + let result = api.delete_robot(robot_info.header.id).await; + //assert!(result.is_ok()); + let _robot = unwrap_factory2(result); + Ok(()) +} + +#[cfg(feature = "robocraft2")] +#[tokio::test] +async fn robocraft2_factory_my_bots() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + let result = api.my_robots().await; + assert!(result.is_ok()); + let robo_info = unwrap_factory2(result); + assert_ne!(robo_info.vehicles.len(), 0); + for robot in &robo_info.vehicles { + assert_ne!(robot.name, ""); + assert_ne!(robot.creator_id, ""); + assert_ne!(robot.creator_id, ""); + assert_ne!(robot.image, ""); + println!("My bot `{}`", robot.to_string()); + //println!("my vehicle {}", serde_json::to_string_pretty(&robot).unwrap()); + } + Ok(()) +} + +#[cfg(feature = "robocraft2")] +#[tokio::test] +async fn robocraft2_factory_my_published_bots() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + let result = api.my_published_robots().await; + assert!(result.is_ok()); + let robo_info = unwrap_factory2(result); + //assert_ne!(robo_info.vehicles.len(), 0); + for robot in &robo_info.vehicles { + assert_ne!(robot.name, ""); + assert_ne!(robot.creator_id, ""); + assert_ne!(robot.creator_id, ""); + assert_ne!(robot.image, ""); + println!("My pub bot `{}`", robot.to_string()); + //println!("pub vehicle {}", serde_json::to_string_pretty(&robot).unwrap()); + } + Ok(()) +} + +#[cfg(feature = "robocraft2")] +//#[tokio::test] +async fn robocraft2_factory_bot() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + let result = api.get("08dab2c9-7a72-4ec4-843c-154fe8768e91".to_owned()).await; + //assert!(result.is_ok()); + let robot = unwrap_factory2(result); + assert_ne!(robot.header.name, ""); + assert_ne!(robot.header.creator_id, ""); + assert_ne!(robot.header.creator_id, ""); + //assert_ne!(robot.header.image, ""); + //assert_ne!(robot.description, ""); + assert_ne!(robot.data, ""); + println!("robot {}", serde_json::to_string_pretty(&robot).unwrap()); + Ok(()) +} + +#[cfg(feature = "robocraft2")] +//#[tokio::test] +async fn robocraft2_factory_delete_bot() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + let result = api.delete_robot("08dab2d2-dcae-4f52-8a77-bbce7cf10124".to_owned()).await; + //assert!(result.is_ok()); + let _robot = unwrap_factory2(result); + Ok(()) +} + +#[cfg(feature = "robocraft2")] +//#[tokio::test] +async fn robocraft2_factory_unpublish_bot() -> Result<(), ()> { + let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::with_username("FJAPIC00L", "P4$$w0rd").await.unwrap())); + let result = api.unpublish_bot("08dab2d3-2a68-48c6-8fd6-a59a663336ca".to_owned()).await; + //assert!(result.is_ok()); + let _robot = unwrap_factory2(result); + Ok(()) +} + +fn unwrap_factory2(result: Result) -> T { + match result { + Ok(t) => t, + Err(e) => { + //println!("FactoryError: {}", e); + panic!("CRF2 Error: {}", e); + } + } +}