@@ -1,6 +1,6 @@ | |||
[package] | |||
name = "libfj" | |||
version = "0.6.1" | |||
version = "0.7.0" | |||
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] | |||
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"] |
@@ -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<D: for<'a> serde::Deserialize<'a>>(response: Response) -> Result<D, FactoryError> { | |||
let status_code: u16 = response.status().into(); | |||
if status_code > 199 && status_code < 300 { | |||
Ok(response.json::<D>().await.map_err(FactoryError::Protocol)?) | |||
} else { | |||
match response.json::<ErrorPayload>().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<SearchResponse, Error> { | |||
pub async fn list(&self) -> Result<SearchResponse, FactoryError> { | |||
self.search(SearchPayload::default()).await | |||
} | |||
pub async fn search(&self, params: SearchPayload) -> Result<SearchResponse, Error> { | |||
/// Search for robots on the CRF which meet the provided parameters | |||
pub async fn search(&self, params: SearchPayload) -> Result<SearchResponse, FactoryError> { | |||
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::<SearchResponse>(result).await | |||
} | |||
pub async fn create_robot(&self, robot: CreateRobotPayload) -> Result<CreateRobotResponse, FactoryError> { | |||
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::<CreateRobotResponse>(result).await | |||
} | |||
pub async fn publish_robot(&self, robot: PublishRobotPayload, id: String) -> Result<PublishRobotResponse, FactoryError> { | |||
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::<ErrorPayload>().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::<ErrorPayload>().await { | |||
Ok(err) => Err(FactoryError::Response(err)), | |||
Err(e) => Err(FactoryError::ResponseCode(e, status_code)) | |||
} | |||
} | |||
let result = request_builder.send().await?; | |||
result.json::<SearchResponse>().await | |||
} | |||
pub async fn factory_info(&self) -> Result<FactoryInfoResponse, FactoryError> { | |||
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::<FactoryInfoResponse>(result).await | |||
} | |||
pub async fn my_robots(&self) -> Result<MyRobotsResponse, FactoryError> { | |||
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::<MyRobotsResponse>(result).await | |||
} | |||
pub async fn my_published_robots(&self) -> Result<MyRobotsResponse, FactoryError> { | |||
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::<MyRobotsResponse>(result).await | |||
} | |||
pub async fn get(&self, id: String) -> Result<GetRobotResponse, 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.get(url) | |||
.header("Authorization", "Bearer ".to_owned() + &token) | |||
.send().await | |||
.map_err(FactoryError::Protocol)?; | |||
handle_json_response::<GetRobotResponse>(result).await | |||
} | |||
} |
@@ -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<usize, usize>, | |||
#[serde(rename = "materialsUsed")] | |||
pub materials_used: std::collections::HashSet<isize>, | |||
#[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<RobotInfo>, | |||
} | |||
// 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 | |||
} |
@@ -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}; |
@@ -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<String, ()>; | |||
async fn token(&mut self) -> Result<String, Error>; | |||
} | |||
/// 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<Self, Error> { | |||
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::<PortalCheckResponse>().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<Self, Error> { | |||
@@ -96,7 +99,7 @@ impl PortalTokenProvider { | |||
/// Automatically validate portal | |||
async fn auto_portal(client: Client, value: String, token: String) -> Result<Self, Error> { | |||
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::<PortalCheckResponse>().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<Self, Error> { | |||
async fn login_internal(token_data: PortalCheckResponse, client: Client, target: String) -> Result<Self, Error> { | |||
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<ProgressionLoginResponse, Error> { | |||
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::<ProgressionLoginResponse>().await?; | |||
Ok(Self { | |||
token: progress_res, | |||
jwt: token_data, | |||
client: client, | |||
}) | |||
progress_response.json::<ProgressionLoginResponse>().await | |||
} | |||
/// Login using the portal token data from a previous portal authentication | |||
pub async fn login(token_data: PortalCheckResponse) -> Result<Self, Error> { | |||
Self::login_internal(token_data, Client::new()).await | |||
pub async fn login(token_data: PortalCheckResponse, target: String) -> Result<Self, Error> { | |||
Self::login_internal(token_data, Client::new(), target).await | |||
} | |||
pub fn get_account_info(&self) -> Result<AccountInfo, Error> { | |||
@@ -156,13 +164,27 @@ impl PortalTokenProvider { | |||
#[async_trait::async_trait] | |||
impl ITokenProvider for PortalTokenProvider { | |||
async fn token(&mut self) -> Result<String, ()> { | |||
// TODO re-authenticate when expired | |||
if let Some(token) = self.token.token.clone() { | |||
Ok(token) | |||
} else { | |||
Err(()) | |||
async fn token(&mut self) -> Result<String, Error> { | |||
let decoded_jwt = self.jwt.decode_jwt_data(); | |||
let expiry = DateTime::<Utc>::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::<PortalCheckResponse>().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<String>, | |||
} | |||
#[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 { | |||
@@ -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<T>(result: Result<T, robocraft2::FactoryError>) -> T { | |||
match result { | |||
Ok(t) => t, | |||
Err(e) => { | |||
//println!("FactoryError: {}", e); | |||
panic!("CRF2 Error: {}", e); | |||
} | |||
} | |||
} |