From dde005b371f5e9f3152e5b9b67f255869cd68d15 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 8 Feb 2026 17:21:32 -0500 Subject: [PATCH] Add web adapter for vehicle factory --- Cargo.lock | 4 +- Cargo.toml | 4 +- rc_core/src/factory/adapter_enum.rs | 10 ++ rc_core/src/persist/vehicle_factory.rs | 7 ++ rc_factory/src/lib.rs | 1 + rc_factory/src/web/adapter.rs | 151 +++++++++++++++++++++++++ rc_factory/src/web/mod.rs | 4 + 7 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 rc_factory/src/web/adapter.rs create mode 100644 rc_factory/src/web/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 9e159b8..d5be6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2032,9 +2032,7 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libfj" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f55afe6b910429afcd02d7810760e07e8489ad662f7df0e938a6154fe413d41" +version = "0.10.0" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index 1d7e082..fd7e52b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ members = [ [workspace.dependencies] actix-web = { version = "4", default-features = false, features = [ "macros", "compress-brotli", "compress-gzip", "compress-zstd", "ws"] } actix-files = "0.6" -#libfj = { version = "0.9", path = "../libfj" } -libfj = { version = "0.9" } +libfj = { version = "0.10", path = "../libfj" } +#libfj = { version = "0.9" } log = "0.4" env_logger = "0.11" clap = { version = "4.5", features = [ "derive" ] } diff --git a/rc_core/src/factory/adapter_enum.rs b/rc_core/src/factory/adapter_enum.rs index b05c28a..2fec3a1 100644 --- a/rc_core/src/factory/adapter_enum.rs +++ b/rc_core/src/factory/adapter_enum.rs @@ -1,6 +1,7 @@ pub enum Factory { Arc(oj_rc_factory::arc::ArcAdapter), Primary(oj_rc_database::FactoryDatabase), + Web(oj_rc_factory::web::WebAdapter), Custom(Box), None, } @@ -11,6 +12,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.vehicle(id).await, Self::Primary(x) => x.vehicle(id).await, + Self::Web(x) => x.vehicle(id).await, Self::Custom(x) => x.vehicle(id).await, Self::None => Ok(None), } @@ -20,6 +22,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.list(query).await, Self::Primary(x) => x.list(query).await, + Self::Web(x) => x.list(query).await, Self::Custom(x) => x.list(query).await, Self::None => Ok(Vec::default()), } @@ -29,6 +32,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.upload(vehicle).await, Self::Primary(x) => x.upload(vehicle).await, + Self::Web(x) => x.upload(vehicle).await, Self::Custom(x) => x.upload(vehicle).await, Self::None => Ok(oj_rc_factory::VehicleThumbnailInfo { id: i32::MIN, @@ -42,6 +46,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.rate_vehicle(id, combat, cosmetic).await, Self::Primary(x) => x.rate_vehicle(id, combat, cosmetic).await, + Self::Web(x) => x.rate_vehicle(id, combat, cosmetic).await, Self::Custom(x) => x.rate_vehicle(id, combat, cosmetic).await, Self::None => Ok(()), } @@ -51,6 +56,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.purchase(id).await, Self::Primary(x) => x.purchase(id).await, + Self::Web(x) => x.purchase(id).await, Self::Custom(x) => x.purchase(id).await, Self::None => Ok(()), } @@ -60,6 +66,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.update_vehicle(id, cube_data, colour_data).await, Self::Primary(x) => x.update_vehicle(id, cube_data, colour_data).await, + Self::Web(x) => x.update_vehicle(id, cube_data, colour_data).await, Self::Custom(x) => x.update_vehicle(id, cube_data, colour_data).await, Self::None => Ok(()), } @@ -69,6 +76,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.remove_vehicle(id, user_id).await, Self::Primary(x) => x.remove_vehicle(id, user_id).await, + Self::Web(x) => x.remove_vehicle(id, user_id).await, Self::Custom(x) => x.remove_vehicle(id, user_id).await, Self::None => Ok(()), } @@ -78,6 +86,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { match self { Self::Arc(x) => x.set_featured(id, is_featured).await, Self::Primary(x) => x.set_featured(id, is_featured).await, + Self::Web(x) => x.set_featured(id, is_featured).await, Self::Custom(x) => x.set_featured(id, is_featured).await, Self::None => Ok(()), } @@ -89,6 +98,7 @@ impl Factory { Ok(match &conf.adapter { crate::persist::AdapterSettings::Arc(x) => Self::Arc(oj_rc_factory::arc::ArcAdapter::init(&x.uri, x.show_expired, settings.cdn_url.to_owned(), x.override_cdn, x.spoof_username).await?), crate::persist::AdapterSettings::BuiltIn => Self::Primary(builtin_factory_provider()), + crate::persist::AdapterSettings::Web(x) => Self::Web(oj_rc_factory::web::WebAdapter::init(&x.url, &settings.auth_url).await?), crate::persist::AdapterSettings::None => Self::None, }) } diff --git a/rc_core/src/persist/vehicle_factory.rs b/rc_core/src/persist/vehicle_factory.rs index 52ff28c..5bf5b3a 100644 --- a/rc_core/src/persist/vehicle_factory.rs +++ b/rc_core/src/persist/vehicle_factory.rs @@ -25,6 +25,8 @@ pub enum AdapterSettings { Arc(ArcFactorySettings), #[serde(alias = "integrated")] BuiltIn, + #[serde(alias = "online")] + Web(WebFactorySettings), None, } @@ -42,3 +44,8 @@ pub struct ArcFactorySettings { fn default_true() -> bool { true } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct WebFactorySettings { + pub url: String, +} diff --git a/rc_factory/src/lib.rs b/rc_factory/src/lib.rs index 0d17b7d..af7f865 100644 --- a/rc_factory/src/lib.rs +++ b/rc_factory/src/lib.rs @@ -1,4 +1,5 @@ pub mod arc; +pub mod web; mod traits; pub use traits::{VehicleFactoryAdapter, VehicleInfo, VehicleQueryInfo, VehicleUploadInfo, VehicleThumbnailInfo}; diff --git a/rc_factory/src/web/adapter.rs b/rc_factory/src/web/adapter.rs new file mode 100644 index 0000000..b7af4dc --- /dev/null +++ b/rc_factory/src/web/adapter.rs @@ -0,0 +1,151 @@ +pub struct WebAdapter { + api: libfj::robocraft::FactoryAPI, +} + +// TODO do some real authentication +struct FederatedFactoryAuthProvider { + target_domain: String, + my_auth_url: String, +} + +impl libfj::robocraft::ITokenProvider for FederatedFactoryAuthProvider { + fn token(&self) -> Result { + Ok(format!("{},{}", self.target_domain, self.my_auth_url)) + } + + fn scheme(&self) -> &'static str { + "OJWeb" + } +} + +impl WebAdapter { + pub async fn init(url: &str, auth_url: &str) -> Result> { + Ok(Self { + api: libfj::robocraft::FactoryAPI::with_auth(FederatedFactoryAuthProvider { + target_domain: url.to_owned(), + my_auth_url: auth_url.to_owned(), + }) + .with_domain(url)?, + }) + } +} + +#[async_trait::async_trait] +impl crate::VehicleFactoryAdapter for WebAdapter { + async fn vehicle(&self, id: i32) -> Result, Box> { + use base64::Engine; + let id_usize: usize = id.try_into()?; + let response = self.api.get(id_usize).await?; + let response_data = response.response; + Ok(Some(( + crate::VehicleInfo { + id, + cube_data: base64::prelude::BASE64_STANDARD.decode(response_data.cube_data.as_bytes()).unwrap_or_default(), + colour_data: base64::prelude::BASE64_STANDARD.decode(response_data.colour_data.as_bytes()).unwrap_or_default(), + }, + crate::VehicleQueryInfo { + id: response_data.item_id as _, + name: response_data.item_name, + description: response_data.item_description, + thumbnail: response_data.thumbnail, + added_by: response_data.added_by, + added_by_display_name: response_data.added_by_display_name, + added_date: crate::traits::parse_rc_date(&response_data.added_date).unwrap_or_default(), + expiry_date: crate::traits::parse_rc_date(&response_data.expiry_date).unwrap_or_default(), + cpu: response_data.cpu as _, + total_robot_ranking: response_data.total_robot_ranking as _, + rent_count: response_data.rent_count as _, + buy_count: response_data.buy_count as _, + buyable: response_data.buyable, + removed_date: response_data.removed_date.and_then(|x| crate::traits::parse_rc_date(&x).ok()), + ban_date: response_data.ban_date.and_then(|x| crate::traits::parse_rc_date(&x).ok()), + featured: response_data.featured, + banner_message: response_data.banner_message, + combat_rating: response_data.combat_rating as _, + cosmetic_rating: response_data.cosmetic_rating as _, + cube_amounts: serde_json::from_str(&response_data.cube_amounts).unwrap_or_default(), + } + ))) + } + + async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box> { + let response = if query.default_page { + self.api.list().await? + } else { + self.api.list_builder() + .page(query.page as _) + .items_per_page(query.page_size as _) + .order(query.order) + .movement_raw(concat_u32_enums_to_str(&query.movement_filter)) + .weapon_raw(concat_u32_enums_to_str(&query.weapon_filter)) + .min_cpu(query.minimum_cpu as _) + .max_cpu(query.maximum_cpu as _) + .text(query.text_filter) + .text_search_type(if query.player_filter { libfj::robocraft::FactoryTextSearchField::Player } else { query.text_search_field }) + .buyable(query.buyable) + .prepend_featured(query.prepend_featured_robot) + .default_page(query.default_page) + .send().await? + }; + Ok(response.response.roboshop_items.into_iter().map(|response_data| crate::VehicleQueryInfo { + id: response_data.item_id as _, + name: response_data.item_name, + description: response_data.item_description, + thumbnail: response_data.thumbnail, + added_by: response_data.added_by, + added_by_display_name: response_data.added_by_display_name, + added_date: crate::traits::parse_rc_date(&response_data.added_date).unwrap_or_default(), + expiry_date: crate::traits::parse_rc_date(&response_data.expiry_date).unwrap_or_default(), + cpu: response_data.cpu as _, + total_robot_ranking: response_data.total_robot_ranking as _, + rent_count: response_data.rent_count as _, + buy_count: response_data.buy_count as _, + buyable: response_data.buyable, + removed_date: response_data.removed_date.and_then(|x| crate::traits::parse_rc_date(&x).ok()), + ban_date: response_data.ban_date.and_then(|x| crate::traits::parse_rc_date(&x).ok()), + featured: response_data.featured, + banner_message: response_data.banner_message, + combat_rating: response_data.combat_rating as _, + cosmetic_rating: response_data.cosmetic_rating as _, + cube_amounts: serde_json::from_str(&response_data.cube_amounts).unwrap_or_default(), + }).collect()) + } + + async fn upload(&self, _vehicle: crate::VehicleUploadInfo) -> Result>{ + Err("Uploading is not supported in the web adapter".into()) + } + + async fn rate_vehicle(&self, _id: i32, _combat: i32, _cosmetic: i32) -> Result<(), Box> { + Err("Rating is not supported in the web adapter".into()) + } + + /// Just update any purchase trackers + async fn purchase(&self, _id: i32) -> Result<(), Box> { + Ok(()) + } + + async fn update_vehicle(&self, _id: i32, _cube_data: Option>, _colour_data: Option>) -> Result<(), Box> { + Err("Updating vehicles is not supported in the web adapter".into()) + } + + async fn remove_vehicle(&self, _id: i32, _user_id: i32) -> Result<(), Box> { + Err("Removing vehicles is not supported in the web adapter".into()) + } + + async fn set_featured(&self, _id: i32, _is_featured: bool) -> Result<(), Box> { + Err("Featuring vehicles is not supported in the web adapter".into()) + } +} + +pub fn concat_u32_enums_to_str(enums: &[u32]) -> String { + let mut out = String::new(); + for num in enums.iter() { + if out.is_empty() { + out += &num.to_string(); + } else { + out += ","; + out += &num.to_string(); + } + } + out +} diff --git a/rc_factory/src/web/mod.rs b/rc_factory/src/web/mod.rs new file mode 100644 index 0000000..9ac8d29 --- /dev/null +++ b/rc_factory/src/web/mod.rs @@ -0,0 +1,4 @@ +//! Web client adapter for the factory. +//! Not to be confused with rc_factory_web, which provides the server part of this API. +mod adapter; +pub use adapter::WebAdapter;