@@ -13,3 +13,5 @@ pub mod robocraft_simple; | |||
pub mod techblox; | |||
#[cfg(feature = "convert")] | |||
pub mod convert; | |||
#[cfg(feature = "robocraft")] | |||
pub mod robocraft2; |
@@ -85,7 +85,7 @@ pub(crate) struct AuthenticationUsernamePayload { | |||
pub password: String, | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
#[derive(Deserialize, Serialize, Clone, Debug)] | |||
pub(crate) struct AuthenticationResponseInfo { | |||
#[serde(rename = "Token")] | |||
pub token: String, | |||
@@ -16,7 +16,7 @@ pub use self::cubes::{Cube, Cubes}; | |||
mod auth; | |||
pub use self::auth::{ITokenProvider, DefaultTokenProvider}; | |||
mod account; | |||
pub(crate) mod account; | |||
pub use self::account::{AuthenticatedTokenProvider, AccountInfo}; | |||
/// Token defined in a javascript file from Freejam which never expires | |||
@@ -0,0 +1,52 @@ | |||
use reqwest::{Client, Error}; | |||
use url::{Url}; | |||
use crate::robocraft::{ITokenProvider, DefaultTokenProvider}; | |||
use crate::robocraft2::{SearchPayload, SearchResponse}; | |||
/// Community Factory Robot 2 root URL | |||
pub const FACTORY_DOMAIN: &str = "https://factory.production.robocraft2.com"; | |||
/// CRF API implementation | |||
pub struct FactoryAPI { | |||
client: Client, | |||
token: Box<dyn ITokenProvider>, | |||
} | |||
impl FactoryAPI { | |||
/// Create a new instance, using `DefaultTokenProvider`. | |||
pub fn new() -> FactoryAPI { | |||
FactoryAPI { | |||
client: Client::new(), | |||
token: Box::new(DefaultTokenProvider{}), | |||
} | |||
} | |||
/// Create a new instance using the provided token provider. | |||
pub fn with_auth(token_provider: Box<dyn ITokenProvider>) -> FactoryAPI { | |||
FactoryAPI { | |||
client: Client::new(), | |||
token: token_provider, | |||
} | |||
} | |||
/// Retrieve CRF robots on the main page. | |||
/// | |||
/// For searching, use `list_builder()` instead. | |||
pub async fn list(&self) -> Result<SearchResponse, Error> { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
.join("/v1/foundry/search") | |||
.unwrap(); | |||
let mut request_builder = self.client.get(url); | |||
if let Ok(token) = self.token.token() { | |||
request_builder = request_builder.header("Authorization", "Bearer ".to_owned() + &token); | |||
} | |||
let result = request_builder.send().await?; | |||
result.json::<SearchResponse>().await | |||
} | |||
async fn search(&self, params: SearchPayload) -> Result<SearchResponse, Error> { | |||
todo!() | |||
} | |||
} |
@@ -0,0 +1,132 @@ | |||
use serde::{Deserialize, Serialize}; | |||
// search endpoint | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct SearchPayload { | |||
#[serde(rename = "text")] | |||
pub text: Option<String>, | |||
#[serde(rename = "baseCpuMinimum")] | |||
pub base_minimum_cpu: Option<isize>, | |||
#[serde(rename = "baseCpuMaximum")] | |||
pub base_maximum_cpu: Option<isize>, | |||
#[serde(rename = "weaponCpuMinimum")] | |||
pub weapon_minimum_cpu: Option<isize>, | |||
#[serde(rename = "weaponCpuMaximum")] | |||
pub weapon_maximum_cpu: Option<isize>, | |||
#[serde(rename = "cosmeticCpuMinimum")] | |||
pub cosmetic_minimum_cpu: Option<isize>, | |||
#[serde(rename = "cosmeticCpuMaximum")] | |||
pub cosmetic_maximum_cpu: Option<isize>, | |||
#[serde(rename = "clusterMinimum")] | |||
pub cluster_minimum: Option<isize>, | |||
#[serde(rename = "clusterMaximum")] | |||
pub cluster_maximum: Option<isize>, | |||
#[serde(rename = "dateMinimum")] | |||
pub date_minimum: Option<String>, | |||
#[serde(rename = "dateMaximum")] | |||
pub date_maximum: Option<String>, | |||
#[serde(rename = "creatorId")] | |||
pub creator_id: Option<String>, // GUID | |||
#[serde(rename = "page")] | |||
pub page: Option<isize>, | |||
#[serde(rename = "count")] | |||
pub count: Option<isize>, | |||
#[serde(rename = "sortBy")] | |||
pub sort_by: String, | |||
#[serde(rename = "orderBy")] | |||
pub order_by: String, | |||
} | |||
impl Default for SearchPayload { | |||
fn default() -> Self { | |||
Self { | |||
text: None, | |||
base_minimum_cpu: None, | |||
base_maximum_cpu: None, | |||
weapon_minimum_cpu: None, | |||
weapon_maximum_cpu: None, | |||
cosmetic_minimum_cpu: None, | |||
cosmetic_maximum_cpu: None, | |||
cluster_minimum: None, | |||
cluster_maximum: None, | |||
date_minimum: None, | |||
date_maximum: None, | |||
creator_id: None, | |||
page: None, | |||
count: None, | |||
sort_by: "default".to_owned(), | |||
order_by: "ascending".to_owned(), | |||
} | |||
} | |||
} | |||
#[derive(Deserialize, Serialize, Clone, Debug)] | |||
pub struct SearchResponse { | |||
#[serde(rename = "results")] | |||
pub results: Vec<SearchResponseItem>, | |||
} | |||
#[derive(Deserialize, Serialize, Clone, Debug)] | |||
pub struct SearchResponseItem { | |||
#[serde(rename = "robot")] | |||
pub robot: RobotInfo, | |||
#[serde(rename = "prices")] | |||
pub prices: Vec<RobotPrice>, | |||
#[serde(rename = "purchased")] | |||
pub purchased: bool, | |||
} | |||
#[derive(Deserialize, Serialize, Clone, Debug)] | |||
pub struct RobotInfo { | |||
#[serde(rename = "id")] | |||
pub id: String, // GUID | |||
#[serde(rename = "name")] | |||
pub name: String, | |||
#[serde(rename = "creatorId")] | |||
pub creator_id: String, // GUID | |||
#[serde(rename = "creatorName")] | |||
pub creator_name: String, | |||
#[serde(rename = "created")] | |||
pub created: String, // date | |||
#[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, | |||
} | |||
impl std::string::ToString for RobotInfo { | |||
fn to_string(&self) -> String { | |||
format!("{} by {} ({})", &self.name, &self.creator_name, &self.id) | |||
} | |||
} | |||
#[derive(Deserialize, Serialize, Clone, Debug)] | |||
pub struct RobotPrice { | |||
#[serde(rename = "currency")] | |||
pub currency: isize, | |||
#[serde(rename = "amount")] | |||
pub amount: isize, | |||
} |
@@ -0,0 +1,11 @@ | |||
//! Robocraft2 APIs for the CRF. | |||
//! Subject to change and breakages as RC2 is still in an early development stage. | |||
mod factory; | |||
pub use factory::FactoryAPI; | |||
mod factory_json; | |||
pub use factory_json::{SearchPayload, SearchResponse, SearchResponseItem, RobotInfo, RobotPrice}; | |||
mod portal; | |||
pub use self::portal::PortalTokenProvider; |
@@ -0,0 +1,119 @@ | |||
use std::sync::RwLock; | |||
use serde::{Deserialize, Serialize}; | |||
use ureq::{Agent, Error}; | |||
use serde_json::to_string; | |||
use crate::robocraft::{ITokenProvider, account::AuthenticationResponseInfo, AccountInfo}; | |||
/// Token provider for an existing Freejam account, authenticated through the web browser portal. | |||
/// | |||
/// Steam and Epic accounts are not supported. | |||
pub struct PortalTokenProvider { | |||
/// Login token | |||
token: RwLock<ProgressionLoginResponse>, | |||
/// User info token | |||
jwt: AuthenticationResponseInfo, | |||
/// Ureq HTTP client | |||
client: Agent, | |||
} | |||
impl PortalTokenProvider { | |||
pub fn portal() -> Result<Self, Error> { | |||
Self::target("Techblox".to_owned()) | |||
} | |||
pub fn target(value: String) -> Result<Self, Error> { | |||
let client = Agent::new(); | |||
let payload = PortalStartPayload { | |||
target: value, | |||
}; | |||
let start_response = client.post("https://account.freejamgames.com/api/authenticate/portal/start") | |||
.set("Content-Type", "application/json") | |||
.send_string(&to_string(&payload).unwrap())?; | |||
let start_res = start_response.into_json::<PortalStartResponse>()?; | |||
println!("GO TO https://account.freejamgames.com/login?theme=rc2&redirect_url=portal?theme=rc2%26portalToken={}", start_res.token); | |||
let payload = PortalCheckPayload { | |||
token: start_res.token, | |||
}; | |||
let mut check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check") | |||
.set("Content-Type", "application/json") | |||
.send_json(&payload)?; | |||
//.send_string(&to_string(&payload).unwrap())?; | |||
let mut auth_complete = check_response.status() == 200; | |||
while !auth_complete { | |||
check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check") | |||
.set("Content-Type", "application/json") | |||
.send_json(&payload)?; | |||
auth_complete = check_response.status() == 200; | |||
} | |||
let check_res = check_response.into_json::<AuthenticationResponseInfo>()?; | |||
// login with token we just got | |||
let payload = ProgressionLoginPayload { | |||
token: check_res.token.clone(), | |||
}; | |||
let progress_response = client.post("https://progression.production.robocraft2.com/login/fj") | |||
.set("Content-Type", "application/json") | |||
.send_json(&payload)?; | |||
let progress_res = progress_response.into_json::<ProgressionLoginResponse>()?; | |||
Ok(Self { | |||
token: RwLock::new(progress_res), | |||
jwt: check_res, | |||
client: client, | |||
}) | |||
} | |||
pub fn get_account_info(&self) -> Result<AccountInfo, Error> { | |||
Ok(self.jwt.decode_jwt_data()) | |||
} | |||
} | |||
impl ITokenProvider for PortalTokenProvider { | |||
fn token(&self) -> Result<String, ()> { | |||
// TODO re-authenticate when expired | |||
if let Some(token) = self.token.read().map_err(|_| ())?.token.clone() { | |||
Ok(token) | |||
} else { | |||
Err(()) | |||
} | |||
} | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub(crate) struct PortalStartPayload { | |||
#[serde(rename = "Target")] | |||
pub target: String, | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub(crate) struct PortalStartResponse { | |||
#[serde(rename = "Token")] | |||
pub token: String, | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub(crate) struct PortalCheckPayload { | |||
#[serde(rename = "Token")] | |||
pub token: String, | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub(crate) struct ProgressionLoginPayload { | |||
#[serde(rename = "token")] | |||
pub token: String, | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub(crate) struct ProgressionLoginResponse { | |||
#[serde(rename = "success")] | |||
pub success: bool, | |||
#[serde(rename = "error")] | |||
pub error: Option<String>, | |||
#[serde(rename = "token")] | |||
pub token: Option<String>, | |||
#[serde(rename = "serverToken")] | |||
pub server_token: Option<String>, | |||
} |
@@ -1,6 +1,8 @@ | |||
#[cfg(feature = "robocraft")] | |||
use libfj::robocraft; | |||
#[cfg(feature = "robocraft")] | |||
use libfj::robocraft2; | |||
#[cfg(feature = "robocraft")] | |||
use libfj::robocraft::ITokenProvider; | |||
#[cfg(feature = "robocraft")] | |||
@@ -30,3 +32,19 @@ fn robocraft_account() -> Result<(), ()> { | |||
assert_eq!(account.created_date, "2019-01-18T14:48:09"); | |||
Ok(()) | |||
} | |||
// this requires human-interaction so it's disabled by default | |||
#[cfg(feature = "robocraft")] | |||
#[allow(dead_code)] | |||
//#[test] | |||
fn robocraft2_account() -> Result<(), ()> { | |||
let token_maybe = robocraft2::PortalTokenProvider::portal(); | |||
assert!(token_maybe.is_ok()); | |||
let token_provider = token_maybe.unwrap(); | |||
let account_maybe = token_provider.get_account_info(); | |||
assert!(account_maybe.is_ok()); | |||
let account = account_maybe.unwrap(); | |||
assert_eq!(account.display_name, "NGniusness"); | |||
assert_eq!(account.created_date, "2014-09-17T21:02:46"); | |||
Ok(()) | |||
} |
@@ -1,6 +1,8 @@ | |||
#[cfg(feature = "robocraft")] | |||
use libfj::robocraft; | |||
#[cfg(feature = "robocraft")] | |||
use libfj::robocraft2; | |||
#[cfg(feature = "robocraft")] | |||
use std::convert::From; | |||
#[cfg(feature = "robocraft")] | |||
@@ -29,6 +31,24 @@ async fn robocraft_factory_default_query() -> Result<(), ()> { | |||
Ok(()) | |||
} | |||
#[cfg(feature = "robocraft")] | |||
#[tokio::test] | |||
async fn robocraft2_factory_default_query() -> Result<(), ()> { | |||
let api = robocraft2::FactoryAPI::with_auth(Box::new(robocraft2::PortalTokenProvider::portal().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() | |||