diff --git a/Cargo.lock b/Cargo.lock index 832b4c4..31b7377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2585,6 +2585,8 @@ dependencies = [ "libfj", "log", "sea-orm", + "serde_json", + "tokio", ] [[package]] diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index ce2c195..25f4f49 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -14521,7 +14521,7 @@ "factory": { "adapter": { "variant": "Arc", - "uri": "sqlite:../../arc/rc_archive.db?mode=ro" + "uri": "sqlite:../data/robocraft/rc_archive.db?mode=rw" } }, "settings": { diff --git a/cdn/src/main.rs b/cdn/src/main.rs index 1927964..4c8340e 100644 --- a/cdn/src/main.rs +++ b/cdn/src/main.rs @@ -32,6 +32,8 @@ async fn main() -> std::io::Result<()> { .service(robocraft::brawl_data::get) .service(robocraft::campaign_data::get) .service(robocraft::factory::arc::get) + .service(robocraft::factory::thumbnail::get) + .service(robocraft::factory::thumbnail::post) .service(robocraft::favicon::get) }) .bind((cli_args.ip, cli_args.port))? diff --git a/cdn/src/robocraft/factory/arc.rs b/cdn/src/robocraft/factory/arc.rs index c9c963b..7547b9a 100644 --- a/cdn/src/robocraft/factory/arc.rs +++ b/cdn/src/robocraft/factory/arc.rs @@ -8,7 +8,7 @@ static ZIP_FILE: std::sync::Mutex, id: Path) -> HttpResponse { let id: u32 = *id; let zip_path = std::path::PathBuf::from(&cli.data_robocraft).join("rc_archive_thumbnails.zip"); - let thumb_dir_path = std::path::PathBuf::from(&cli.data_robocraft).join("factorythumbnails"); + let thumb_dir_path = std::path::PathBuf::from(&cli.data_robocraft).join(super::THUMBNAIL_DIR); try_find_file(zip_path, thumb_dir_path, id).await } @@ -79,6 +79,8 @@ fn get_file_in_zip(zip_path: std::path::PathBuf, id: u32) -> zip::result::ZipRes } fn get_file_in_thumbnails(dir: std::path::PathBuf, id: u32) -> std::io::Result> { + // in case someone has extracted the thumbnail zip + // (newly-uploaded vehicles use the general thumbnail CDN endpoint) let prefix = format!("{} - ", id); for ent in std::fs::read_dir(&dir)? { let ent = ent?; diff --git a/cdn/src/robocraft/factory/mod.rs b/cdn/src/robocraft/factory/mod.rs index 02d3672..a7f06c2 100644 --- a/cdn/src/robocraft/factory/mod.rs +++ b/cdn/src/robocraft/factory/mod.rs @@ -1 +1,4 @@ pub mod arc; +pub mod thumbnail; + +const THUMBNAIL_DIR: &str = "factorythumbnails"; diff --git a/cdn/src/robocraft/factory/thumbnail.rs b/cdn/src/robocraft/factory/thumbnail.rs new file mode 100644 index 0000000..7e4d252 --- /dev/null +++ b/cdn/src/robocraft/factory/thumbnail.rs @@ -0,0 +1,22 @@ +use actix_web::{get, post, web::{Data, Path, Bytes}, Responder}; + +#[get("/roboshop/Live/{id}")] +pub async fn get(cli: Data, id: Path) -> impl Responder { + let path = std::path::PathBuf::from(&cli.data_robocraft).join(super::THUMBNAIL_DIR).join(format!("{}.jpg", id)); + log::debug!("RC asset at {} (exists? {})", path.display(), path.exists()); + if path.exists() { + actix_files::NamedFile::open_async(path).await + } else { + log::info!("Not found /roboshop/Live/{} -> {}, using default image", id, path.display()); + actix_files::NamedFile::open_async(std::path::PathBuf::from(&cli.assets_robocraft).join(super::super::DEFAULT_IMAGE)).await + } +} + +#[post("/roboshop/Live/{id}")] +pub async fn post(cli: Data, auth: Data, id: Path, body: Bytes, req: actix_web::HttpRequest) -> Result { + auth.validate(&req, &id.to_string())?; + let path = std::path::PathBuf::from(&cli.data_robocraft).join(super::THUMBNAIL_DIR).join(format!("{}.jpg", *id)); + log::debug!("Saving factory thumbnail for {} to {}: {}B", id, path.display(), body.len()); + std::fs::write(path, &body).map_err(crate::robocraft::IntercomOpError::Io)?; + Ok(actix_web::HttpResponse::NoContent().finish()) +} diff --git a/rc_core/src/factory/adapter_enum.rs b/rc_core/src/factory/adapter_enum.rs index 7188c1b..7228824 100644 --- a/rc_core/src/factory/adapter_enum.rs +++ b/rc_core/src/factory/adapter_enum.rs @@ -22,19 +22,23 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { } } - async fn upload(&self, vehicle: oj_rc_factory::VehicleUploadInfo) -> Result> { + async fn upload(&self, vehicle: oj_rc_factory::VehicleUploadInfo) -> Result> { match self { Self::Arc(x) => x.upload(vehicle).await, Self::Custom(x) => x.upload(vehicle).await, - Self::None => Ok(false), + Self::None => Ok(oj_rc_factory::VehicleThumbnailInfo { + id: i32::MIN, + thumbnail: vehicle.thumbnail, + needs_upload: false, + }), } } } impl Factory { - pub async fn from_config(conf: &crate::persist::FactoryConfig) -> Result> { + pub async fn from_config(conf: &crate::persist::FactoryConfig, settings: &crate::persist::config::ServerConfig) -> Result> { Ok(match &conf.adapter { - crate::persist::AdapterSettings::Arc(x) => Self::Arc(oj_rc_factory::arc::ArcAdapter::init(&x.uri, x.show_expired, x.cdn.clone(), x.override_cdn, x.spoof_username).await?), + 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::None => Self::None, }) } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 6d51cc3..4b2fc0f 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -293,8 +293,8 @@ impl super::ConfigProvider for CubeConfig { auto_signup: self.settings.server.auto_signup, queue_mode: super::QueueChangeMode::from_persist(self.settings.server.queue_mode.clone()), cdn_url: self.settings.server.cdn_url.trim_end_matches('/').to_owned(), - auth_url: self.settings.server.auth_url.trim_matches('/').to_owned(), - intercom_url: self.settings.server.intercom_url.trim_matches('/').to_owned(), + auth_url: self.settings.server.auth_url.trim_end_matches('/').to_owned(), + intercom_url: self.settings.server.intercom_url.trim_end_matches('/').to_owned(), } } @@ -308,7 +308,7 @@ impl super::ConfigProvider for CubeConfig { } async fn factory(&self) -> Result> { - crate::factory::Factory::from_config(&self.factory).await + crate::factory::Factory::from_config(&self.factory, &>::server_config(self)).await } fn cubes(&self) -> &'_ indexmap::IndexMap { diff --git a/rc_core/src/persist/settings.rs b/rc_core/src/persist/settings.rs index e911e1a..f5f626a 100644 --- a/rc_core/src/persist/settings.rs +++ b/rc_core/src/persist/settings.rs @@ -124,7 +124,7 @@ fn default_server_conf() -> ServerSettings { } } -pub fn default_cdn_root_url() -> String { +fn default_cdn_root_url() -> String { "http://127.0.0.1:8010".to_owned() } diff --git a/rc_core/src/persist/user/intercom.rs b/rc_core/src/persist/user/intercom.rs index 643c9df..a0bcac0 100644 --- a/rc_core/src/persist/user/intercom.rs +++ b/rc_core/src/persist/user/intercom.rs @@ -53,6 +53,21 @@ impl super::IntercomUser for super::account_json::UserData { Ok(()) } + async fn save_factory_thumbnail(&self, factory_id: i32, image: Vec) -> Result<(), polariton_server::operations::SimpleOpError> { + let token = generate_token(factory_id.to_string().as_bytes(), &self.secret); + let auth_header_val = format!("Internal {}", token); + let url = format!("{}/roboshop/Live/{}", self.cdn, factory_id); + if let Err(e) = self.http_client.post(url) + .header("Authorization", auth_header_val) + .body(image) + .send() + .await { + log::error!("Failed to update factory thumbnail for {} ({}): {}", self.account.public_id, self.account.id, e); + return Err((crate::data::error_codes::WebServicesError::UnexpectedError as i16).into()); + } + Ok(()) + } + async fn webservice_listener(&self) -> Result, polariton_server::operations::SimpleOpError> { self.listen_on_websocket(".oj_services").await .map_err(|e| polariton_server::operations::SimpleOpError::with_message( diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index aa0073d..b0b02c3 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -336,6 +336,7 @@ pub trait MultiplayerUser: IntercomUser + CommonUser { #[async_trait::async_trait] pub trait IntercomUser: CommonUser { async fn save_custom_avatar(&self, image: Vec) -> Result<(), polariton_server::operations::SimpleOpError>; + async fn save_factory_thumbnail(&self, factory_id: i32, image: Vec) -> Result<(), polariton_server::operations::SimpleOpError>; async fn webservice_listener(&self) -> Result, polariton_server::operations::SimpleOpError>; async fn show_dev_message(&self, msg: super::intercom::IntercomDevMessage, to: Vec); async fn enter_maintenance(&self, msg: super::intercom::IntercomMaintenanceMessage, to: Vec); diff --git a/rc_core/src/persist/vehicle_factory.rs b/rc_core/src/persist/vehicle_factory.rs index 63ad49b..669d412 100644 --- a/rc_core/src/persist/vehicle_factory.rs +++ b/rc_core/src/persist/vehicle_factory.rs @@ -31,19 +31,12 @@ pub struct ArcFactorySettings { pub uri: String, #[serde(default = "default_true")] pub show_expired: bool, - /// should probably end with /roboshop/arc/Live/ - #[serde(default = "default_arc_live_url")] - pub cdn: String, #[serde(default)] pub override_cdn: bool, #[serde(default = "default_true")] pub spoof_username: bool, } -fn default_arc_live_url() -> String { - format!("{}/roboshop/arc/Live/", super::settings::default_cdn_root_url()) -} - fn default_true() -> bool { true } diff --git a/rc_factory/src/arc/adapter.rs b/rc_factory/src/arc/adapter.rs index 33a5048..1bb2189 100644 --- a/rc_factory/src/arc/adapter.rs +++ b/rc_factory/src/arc/adapter.rs @@ -1,7 +1,6 @@ use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, ActiveModelTrait, Set, TransactionTrait}; use base64::Engine; use std::collections::HashMap; -use std::path::Path; pub struct ArcAdapter { orm: sea_orm::DatabaseConnection, @@ -15,11 +14,11 @@ impl ArcAdapter { pub async fn init(uri: &str, show_expired: bool, cdn: String, override_cdn: bool, username_spoofing: bool) -> Result{ log::debug!("Connecting to Archive of RoboCraft (ARC) vehicle factory database URI: {}", uri); let db = sea_orm::Database::connect(uri).await?; - let good_cdn = if cdn.ends_with('/') { cdn } else { format!("{}/", cdn) }; + //let good_cdn = cdn.to_owned()format!("{}/roboshop/Live/", cdn); let adapter = Self { orm: db, ignore_expiry: show_expired, - cdn: good_cdn, + cdn: cdn.to_owned(), override_cdn: override_cdn, spoof_users: username_spoofing, }; @@ -36,7 +35,7 @@ impl ArcAdapter { fn thumbnail_url(&self, meta: String, id: u32) -> String { if self.override_cdn { - format!("{}{}", &self.cdn, id) + format!("{}/roboshop/arc/Live/{}", &self.cdn, id) } else { meta } @@ -187,7 +186,7 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { Ok(infos) } - async fn upload(&self, vehicle: crate::VehicleUploadInfo) -> Result> { + async fn upload(&self, vehicle: crate::VehicleUploadInfo) -> Result> { let transaction = self.orm.begin().await?; let cube_amounts = { @@ -207,17 +206,18 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { serde_json::to_string(&str_map).unwrap_or_else(|_| "{}".to_string()) }; let cubes = super::entities::robot_cubes::ActiveModel { + id: sea_orm::ActiveValue::NotSet, cube_data: Set(base64::prelude::BASE64_STANDARD.encode(&vehicle.cube_data)), colour_data: Set(base64::prelude::BASE64_STANDARD.encode(&vehicle.colour_data)), cube_amounts: Set(cube_amounts), - ..Default::default() }.insert(&transaction).await?; let now = chrono::Utc::now(); - let meta = super::entities::robot_metadata::ActiveModel { + let _meta = super::entities::robot_metadata::ActiveModel { + id: Set(cubes.id), name: Set(vehicle.name), description: Set(vehicle.description), - thumbnail: Set(format!("{}{}", &self.cdn, cubes.id)), + thumbnail: Set("REPLACE ME".to_string()), added_by: Set(vehicle.added_by), added_by_display_name: Set(vehicle.added_by_display_name), added_date: Set(now.format("%Y-%m-%dT%H:%M:%S").to_string()), @@ -230,16 +230,20 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { featured: Set(0), combat_rating: Set(3.0), cosmetic_rating: Set(3.0), - ..Default::default() }.insert(&transaction).await?; + super::entities::robot_metadata::ActiveModel { + id: Set(cubes.id), + thumbnail: Set(format!("{}/roboshop/Live/{}", &self.cdn, cubes.id)), + ..Default::default() + }.update(&transaction).await?; transaction.commit().await?; - let entry_name = format!("{} - {}.jpg", meta.id, meta.name); - let thumbnail = vehicle.thumbnail; - tokio::task::spawn_blocking(move || { - std::fs::write(Path::new("../data/robocraft/factorythumbnails").join(&entry_name), &thumbnail).ok(); - }); + let result = crate::VehicleThumbnailInfo { + id: cubes.id as i32, + thumbnail: vehicle.thumbnail, + needs_upload: true, + }; - Ok(true) + Ok(result) } } diff --git a/rc_factory/src/lib.rs b/rc_factory/src/lib.rs index 6030f05..0d17b7d 100644 --- a/rc_factory/src/lib.rs +++ b/rc_factory/src/lib.rs @@ -1,4 +1,4 @@ pub mod arc; mod traits; -pub use traits::{VehicleFactoryAdapter, VehicleInfo, VehicleQueryInfo, VehicleUploadInfo}; +pub use traits::{VehicleFactoryAdapter, VehicleInfo, VehicleQueryInfo, VehicleUploadInfo, VehicleThumbnailInfo}; diff --git a/rc_factory/src/traits.rs b/rc_factory/src/traits.rs index 93418ef..85d378b 100644 --- a/rc_factory/src/traits.rs +++ b/rc_factory/src/traits.rs @@ -2,7 +2,7 @@ pub trait VehicleFactoryAdapter: Send + Sync + 'static { async fn vehicle(&self, id: u32) -> Result, Box>; async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box>; - async fn upload(&self, vehicle: VehicleUploadInfo) -> Result>; + async fn upload(&self, vehicle: VehicleUploadInfo) -> Result>; } #[derive(Debug, Clone)] @@ -65,3 +65,10 @@ pub struct VehicleUploadInfo { pub colour_data: Vec, pub build_version: String, } + +#[derive(Debug)] +pub struct VehicleThumbnailInfo { + pub id: i32, + pub thumbnail: Vec, + pub needs_upload: bool, +} diff --git a/rc_services_room/src/operations/crf_upload.rs b/rc_services_room/src/operations/crf_upload.rs index 57f11b4..f9f4828 100644 --- a/rc_services_room/src/operations/crf_upload.rs +++ b/rc_services_room/src/operations/crf_upload.rs @@ -15,11 +15,15 @@ async fn do_handling(params: ParameterTable<()>, user: &crate::UserTy, factory: let upload_info = crate::data::crf::UploadData::from_transmissibles(version.string, data)?; let user_info = user.user()?; let prepared = user_info.prepare_factory_upload(upload_info.into_core()).await?; - let success = factory.upload(prepared).await.map_err(|e| { + let result = factory.upload(prepared).await.map_err(|e| { log::error!("Failed to upload to factory: {}", e); oj_rc_core::data::error_codes::WebServicesError::UnexpectedError as i16 })?; - params.insert(SUCCESS_PARAM_KEY, Typed::Bool(success)); + if result.needs_upload { + user_info.save_factory_thumbnail(result.id, result.thumbnail).await?; + } + params.insert(SUCCESS_PARAM_KEY, Typed::Bool(true)); + } } Ok(params.into()) diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 84d2a9b..475a102 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -220,5 +220,6 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(garage_slot_copy::garage_slot_copy_provider()) .add(polariton_server::operations::Ack::<12, _>::default()) // TODO handle UpdatePlayerDailyQuestProgressRequest instead of ignoring it .add(polariton_server::operations::Ack::<90, _>::default()) // TODO handle SubmitCRFRatingRequest instead of ignoring it + //.add(polariton_server::operations::Ack::<97, _>::default()) // TODO handle UpdateShopRobotOffsetRequest instead of ignoring it (this seems to break newly-uploaded vehicles for now) .add(steam_promo::steam_promos_provider()) }