diff --git a/src/lib.rs b/src/lib.rs index e208aaf..a79bcf3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod cardlife; +pub mod robocraft; #[cfg(test)] mod tests {} diff --git a/src/robocraft/auth.rs b/src/robocraft/auth.rs new file mode 100644 index 0000000..aece03d --- /dev/null +++ b/src/robocraft/auth.rs @@ -0,0 +1,15 @@ + +use crate::robocraft::{DEFAULT_TOKEN}; + +pub trait ITokenProvider { + fn token(&self) -> Result; +} + +pub struct DefaultTokenProvider { +} + +impl ITokenProvider for DefaultTokenProvider { + fn token(&self) -> Result { + Ok(DEFAULT_TOKEN.to_string()) + } +} diff --git a/src/robocraft/factory.rs b/src/robocraft/factory.rs new file mode 100644 index 0000000..676468f --- /dev/null +++ b/src/robocraft/factory.rs @@ -0,0 +1,59 @@ +use reqwest::{Client, Error}; +use url::{Url}; + +use crate::robocraft::{ITokenProvider, DefaultTokenProvider, FactoryInfo, FactorySearchBuilder}; +use crate::robocraft::factory_json::ListPayload; + +const FACTORY_DOMAIN: &str = "https://factory.robocraftgame.com/"; + +pub struct FactoryAPI { + client: Client, + token: Box, +} + +impl FactoryAPI { + pub fn new() -> FactoryAPI { + FactoryAPI { + client: Client::new(), + token: Box::new(DefaultTokenProvider{}), + } + } + + pub fn with_auth(token_provider: Box) -> FactoryAPI { + FactoryAPI { + client: Client::new(), + token: token_provider, + } + } + + pub async fn list(&self) -> Result { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join("/api/roboShopItems/list") + .unwrap(); + let payload = ListPayload::default(); + let mut request_builder = self.client.post(url) + .json(&payload); + if let Ok(token) = self.token.token() { + request_builder = request_builder.header("Authorization", "Web ".to_owned() + &token); + } + let result = request_builder.send().await; + if let Ok(response) = result { + return response.json::().await; + } + Err(result.err().unwrap()) + } + + pub fn list_builder(&self) -> FactorySearchBuilder { + let url = Url::parse(FACTORY_DOMAIN) + .unwrap() + .join("/api/roboShopItems/list") + .unwrap(); + let mut token_opt = None; + if let Ok(token) = self.token.token() { + token_opt = Some(token); + } + let request_builder = self.client.post(url); + FactorySearchBuilder::new(request_builder, token_opt) + } +} diff --git a/src/robocraft/factory_json.rs b/src/robocraft/factory_json.rs new file mode 100644 index 0000000..0e8ed2a --- /dev/null +++ b/src/robocraft/factory_json.rs @@ -0,0 +1,145 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone)] +pub(crate) struct ListPayload { + #[serde(rename = "page")] + pub page: isize, + #[serde(rename = "pageSize")] + pub page_size: isize, + #[serde(rename = "order")] + pub order: isize, + #[serde(rename = "playerFilter")] + pub player_filter: bool, + #[serde(rename = "movementFilter")] + pub movement_filter: String, // csv int enums as str + #[serde(rename = "movementCategoryFilter")] + pub movement_category_filter: String, // csv int enums as str + #[serde(rename = "weaponFilter")] + pub weapon_filter: String, // csv int enums as str + #[serde(rename = "weaponCategoryFilter")] + pub weapon_category_filter: String, // csv int enums as str + #[serde(rename = "minimumCpu")] + pub minimum_cpu: isize, + #[serde(rename = "maximumCpu")] + pub maximum_cpu: isize, + #[serde(rename = "textFilter")] + pub text_filter: String, + #[serde(rename = "textSearchField")] + pub text_search_field: isize, // ??? + #[serde(rename = "buyable")] + pub buyable: bool, + #[serde(rename = "prependFeaturedRobot")] + pub prepend_featured_robot: bool, + #[serde(rename = "featuredOnly")] + pub featured_only: bool, + #[serde(rename = "defaultPage")] + pub default_page: bool, +} + +impl ListPayload { + pub fn default() -> ListPayload { + ListPayload { + page: 1, + page_size: 100, + order: 0, + player_filter: false, + movement_filter: "100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000".to_string(), + movement_category_filter: "100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000".to_string(), + weapon_filter: "10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000".to_string(), + weapon_category_filter: "10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000".to_string(), + minimum_cpu: -1, + maximum_cpu: -1, + text_filter: "".to_string(), + text_search_field: 0, + buyable: true, + prepend_featured_robot: false, + featured_only: false, + default_page: true, + } + } + + pub fn empty() -> ListPayload { + ListPayload { + page: 1, + page_size: 100, + order: 0, + player_filter: false, + movement_filter: "".to_string(), + movement_category_filter: "".to_string(), + weapon_filter: "".to_string(), + weapon_category_filter: "".to_string(), + minimum_cpu: -1, + maximum_cpu: -1, + text_filter: "".to_string(), + text_search_field: 0, + buyable: true, + prepend_featured_robot: false, + featured_only: false, + default_page: false, + } + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct FactoryInfo { + #[serde(rename = "response")] + pub response: RoboShopItemsInfo, + #[serde(rename = "statusCode")] + pub status_code: usize, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct RoboShopItemsInfo { + #[serde(rename = "roboShopItems")] + pub roboshop_items: Vec, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct FactoryRobotListInfo { + #[serde(rename = "itemId")] + pub item_id: usize, + #[serde(rename = "itemName")] + pub item_name: String, + #[serde(rename = "itemDescription")] + pub item_description: String, + #[serde(rename = "thumbnail")] + pub thumbnail: String, // url + #[serde(rename = "addedBy")] + pub added_by: String, + #[serde(rename = "addedByDisplayName")] + pub added_by_display_name: String, + #[serde(rename = "addedDate")] + pub added_date: String, // ISO date + #[serde(rename = "expiryDate")] + pub expiry_date: String, // ISO date + #[serde(rename = "cpu")] + pub cpu: usize, + #[serde(rename = "totalRobotRanking")] + pub total_robot_ranking: usize, + #[serde(rename = "rentCount")] + pub rent_count: usize, + #[serde(rename = "buyCount")] + pub buy_count: usize, + #[serde(rename = "buyable")] + pub buyable: bool, + #[serde(rename = "removedDate")] + pub removed_date: Option, + #[serde(rename = "banDate")] + pub ban_date: Option, + #[serde(rename = "featured")] + pub featured: bool, + #[serde(rename = "bannerMessage")] + pub banner_message: Option, + #[serde(rename = "combatRating")] + pub combat_rating: f32, + #[serde(rename = "cosmeticRating")] + pub cosmetic_rating: f32, + #[serde(rename = "cubeAmounts")] + pub cube_amounts: String, // JSON as str +} + +impl std::string::ToString for FactoryRobotListInfo { + fn to_string(&self) -> String { + format!("{} by {} ({})", &self.item_name, &self.added_by_display_name, &self.item_id) + } +} diff --git a/src/robocraft/factory_request_builder.rs b/src/robocraft/factory_request_builder.rs new file mode 100644 index 0000000..0648f29 --- /dev/null +++ b/src/robocraft/factory_request_builder.rs @@ -0,0 +1,170 @@ +use reqwest::{RequestBuilder, Error}; + +use crate::robocraft::{FactoryInfo}; +use crate::robocraft::factory_json::ListPayload; + +pub enum FactoryOrderType { + Suggested = 0, + CombatRating = 1, + CosmeticRating = 2, + Added = 3, + CPU = 4, + MostBought = 5, +} + +pub enum FactoryMovementType { + Wheels = 100000, + Hovers = 200000, + Aerofoils=300000, + Thrusters=400000, + Rudders=500000, + InsectLegs=600000, + MechLegs=700000, + Skis=800000, + TankTreads=900000, + Rotors=1000000, + Sprinters=1100000, + Propellers=1200000 +} + +pub enum FactoryWeaponType { + Laser=10000000, + PlasmaLauncher=20000000, + GyroMortar=25000000, + RailCannon=30000000, + NanoDisruptor=40000000, + TeslaBlade=50000000, + AeroflakCannon=60000000, + IonCannon=65000000, + ProtoSeeker=70100000, + ChainShredder=75000000, +} + +pub enum FactoryTextSearchType { + All=0, + Player=1, + Name=2, +} + +pub struct FactorySearchBuilder { + reqwest_builder: RequestBuilder, + payload: ListPayload, + token: Option, +} + +impl FactorySearchBuilder { + pub(crate) fn new(request_builder: RequestBuilder, token: Option) -> FactorySearchBuilder { + FactorySearchBuilder { + reqwest_builder: request_builder, + payload: ListPayload::empty(), + token, + } + } + + pub fn page(mut self, page_number: isize) -> Self { + self.payload.page = page_number; + self + } + + pub fn items_per_page(mut self, page_size: isize) -> Self { + self.payload.page_size = page_size; + self + } + + pub fn order(mut self, order_type: FactoryOrderType) -> Self { + self.payload.order = order_type as isize; + self + } + + /* // this appears to not do anything (removed to prevent confusion) + // use text_search_type(FactoryTextSearchType::Player) instead + pub fn players_only(mut self, p: bool) -> Self { + self.payload.player_filter = p; + self + } + */ + + pub fn movement_or(mut self, movement_type: FactoryMovementType) -> Self { + if self.payload.movement_filter == "" { + self.payload.movement_filter = format!("{},{}", &self.payload.movement_filter, movement_type as isize); + } else { + self.payload.movement_filter = (movement_type as isize).to_string(); + } + self.payload.movement_category_filter = self.payload.movement_filter.clone(); + self + } + + pub fn weapon_or(mut self, weapon_type: FactoryWeaponType) -> Self { + if self.payload.weapon_filter == "" { + self.payload.weapon_filter = format!("{},{}", &self.payload.weapon_filter, weapon_type as isize); + } else { + self.payload.weapon_filter = (weapon_type as isize).to_string(); + } + self.payload.weapon_category_filter = self.payload.weapon_filter.clone(); + self + } + + pub fn cpu_range(mut self, min: isize, max: isize) -> Self { + self.payload.minimum_cpu = min; + self.payload.maximum_cpu = max; + self + } + + pub fn min_cpu(mut self, min: isize) -> Self { + self.payload.minimum_cpu = min; + self + } + + pub fn max_cpu(mut self, max: isize) -> Self { + self.payload.maximum_cpu = max; + self + } + + pub fn no_minimum_cpu(mut self) -> Self { + self.payload.minimum_cpu = -1; + self + } + + pub fn no_maximum_cpu(mut self) -> Self { + self.payload.maximum_cpu = -1; + self + } + + pub fn text(mut self, t: String) -> Self { + self.payload.text_filter = t; + self + } + + pub fn text_search_type(mut self, search_type: FactoryTextSearchType) -> Self { + self.payload.text_search_field = search_type as isize; + self + } + + // setting buyable to false while using the default token provider will cause HTTP status 500 error + pub fn buyable(mut self, b: bool) -> Self { + self.payload.buyable = b; + self + } + + pub fn prepend_featured(mut self, b: bool) -> Self { + self.payload.prepend_featured_robot = b; + self + } + + pub fn default_page(mut self, b: bool) -> Self { + self.payload.default_page = b; + self + } + + pub async fn send(mut self) -> Result { + self.reqwest_builder = self.reqwest_builder.json(&self.payload); + if let Some(token) = self.token.clone() { + self.reqwest_builder = self.reqwest_builder.header("Authorization", "Web ".to_owned() + &token); + } + let result = self.reqwest_builder.send().await; + if let Ok(response) = result { + return response.json::().await; + } + Err(result.err().unwrap()) + } +} diff --git a/src/robocraft/mod.rs b/src/robocraft/mod.rs new file mode 100644 index 0000000..626c95d --- /dev/null +++ b/src/robocraft/mod.rs @@ -0,0 +1,11 @@ +mod factory; +mod factory_json; +mod factory_request_builder; +pub use self::factory::{FactoryAPI}; +pub use self::factory_json::{FactoryInfo, FactoryRobotListInfo, RoboShopItemsInfo}; +pub use self::factory_request_builder::{FactorySearchBuilder, FactoryMovementType, FactoryOrderType, FactoryWeaponType, FactoryTextSearchType}; + +mod auth; +pub use self::auth::{ITokenProvider, DefaultTokenProvider}; + +pub const DEFAULT_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQdWJsaWNJZCI6IjEyMyIsIkRpc3BsYXlOYW1lIjoiVGVzdCIsIlJvYm9jcmFmdE5hbWUiOiJGYWtlQ1JGVXNlciIsIkZsYWdzIjpbXSwiaXNzIjoiRnJlZWphbSIsInN1YiI6IldlYiIsImlhdCI6MTU0NTIyMzczMiwiZXhwIjoyNTQ1MjIzNzkyfQ.ralLmxdMK9rVKPZxGng8luRIdbTflJ4YMJcd25dKlqg"; diff --git a/tests/clre_server.rs b/tests/clre_server.rs index 99b6e0d..25a5272 100644 --- a/tests/clre_server.rs +++ b/tests/clre_server.rs @@ -6,7 +6,7 @@ fn clre_server_init() -> Result<(), ()> { Ok(()) } -#[tokio::test] +/*#[tokio::test] async fn clre_server_game() -> Result<(), ()> { let server = cardlife::CLreServer::new("http://localhost:5030").unwrap(); let result = server.game_info().await; @@ -36,4 +36,4 @@ async fn clre_server_status() -> Result<(), ()> { } } Ok(()) -} \ No newline at end of file +}*/ diff --git a/tests/robocraft_factory.rs b/tests/robocraft_factory.rs new file mode 100644 index 0000000..a306c8e --- /dev/null +++ b/tests/robocraft_factory.rs @@ -0,0 +1,78 @@ +use libfj::robocraft; + +#[test] +fn robocraft_factory_api_init() -> Result<(), ()> { + robocraft::FactoryAPI::new(); + Ok(()) +} + +#[tokio::test] +async fn robocraft_factory_default_query() -> Result<(), ()> { + let api = robocraft::FactoryAPI::new(); + let result = api.list().await; + assert!(result.is_ok()); + let robo_info = result.unwrap(); + assert_ne!(robo_info.response.roboshop_items.len(), 0); + assert_eq!(robo_info.status_code, 200); + for robot in &robo_info.response.roboshop_items { + assert_ne!(robot.item_name, ""); + assert_ne!(robot.added_by, ""); + assert_ne!(robot.added_by_display_name, ""); + assert_ne!(robot.thumbnail, ""); + println!("FactoryRobotListInfo.to_string() -> `{}`", robot.to_string()); + } + Ok(()) +} + +fn builder() -> robocraft::FactorySearchBuilder { + robocraft::FactoryAPI::new().list_builder() +} + +fn assert_factory_list(robo_info: robocraft::FactoryInfo) -> Result<(), ()> { + assert_ne!(robo_info.response.roboshop_items.len(), 0); + assert_eq!(robo_info.status_code, 200); + for robot in &robo_info.response.roboshop_items { + assert_ne!(robot.item_name, ""); + assert_ne!(robot.added_by, ""); + assert_ne!(robot.added_by_display_name, ""); + assert_ne!(robot.thumbnail, ""); + println!("FactoryRobotListInfo.to_string() -> `{}`", robot.to_string()); + } + Ok(()) +} + +#[tokio::test] +async fn robocraft_factory_custom_query() -> Result<(), ()> { + let api = robocraft::FactoryAPI::new(); + let result = api.list_builder() + .movement_or(robocraft::FactoryMovementType::Wheels) + .weapon_or(robocraft::FactoryWeaponType::Laser) + .page(2) + .items_per_page(10) + .send().await; + assert!(result.is_ok()); + let robo_info = result.unwrap(); + assert_ne!(robo_info.response.roboshop_items.len(), 0); + //assert_eq!(robo_info.response.roboshop_items.len(), 16); the API behaviour is weird, I swear it's not me! + assert!(robo_info.response.roboshop_items.len() >= 10); + assert_eq!(robo_info.status_code, 200); + for robot in &robo_info.response.roboshop_items { + assert_ne!(robot.item_name, ""); + assert_ne!(robot.added_by, ""); + assert_ne!(robot.added_by_display_name, ""); + assert_ne!(robot.thumbnail, ""); + println!("FactoryRobotListInfo.to_string() -> `{}`", robot.to_string()); + } + Ok(()) +} + +#[tokio::test] +async fn robocraft_factory_player_query() -> Result<(), ()> { + let result = builder() + .text("Baerentoeter".to_string()) + .text_search_type(robocraft::FactoryTextSearchType::Player) + .items_per_page(10) + .send().await; + assert!(result.is_ok()); + assert_factory_list(result.unwrap()) +}