Browse Source

Add initial CRF2 support

master
NGnius (Graham) 1 year ago
parent
commit
8e8039df83
9 changed files with 356 additions and 2 deletions
  1. +2
    -0
      src/lib.rs
  2. +1
    -1
      src/robocraft/account.rs
  3. +1
    -1
      src/robocraft/mod.rs
  4. +52
    -0
      src/robocraft2/factory.rs
  5. +132
    -0
      src/robocraft2/factory_json.rs
  6. +11
    -0
      src/robocraft2/mod.rs
  7. +119
    -0
      src/robocraft2/portal.rs
  8. +18
    -0
      tests/robocraft_auth.rs
  9. +20
    -0
      tests/robocraft_factory.rs

+ 2
- 0
src/lib.rs View File

@@ -13,3 +13,5 @@ pub mod robocraft_simple;
pub mod techblox;
#[cfg(feature = "convert")]
pub mod convert;
#[cfg(feature = "robocraft")]
pub mod robocraft2;

+ 1
- 1
src/robocraft/account.rs View File

@@ -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,


+ 1
- 1
src/robocraft/mod.rs View File

@@ -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


+ 52
- 0
src/robocraft2/factory.rs View File

@@ -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!()
}
}

+ 132
- 0
src/robocraft2/factory_json.rs View File

@@ -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,
}

+ 11
- 0
src/robocraft2/mod.rs View File

@@ -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;

+ 119
- 0
src/robocraft2/portal.rs View File

@@ -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>,
}

+ 18
- 0
tests/robocraft_auth.rs View File

@@ -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(())
}

+ 20
- 0
tests/robocraft_factory.rs View File

@@ -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()


Loading…
Cancel
Save