From 956bb2149d6e826321e2178079cb3e6ae00dda56 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 25 May 2025 12:26:08 -0400 Subject: [PATCH] Add avatar saving functionality (without CDN support) --- Cargo.lock | 17 +------ Cargo.toml | 2 +- rc_core/src/persist/user/account_json.rs | 34 ++++++++++++++ rc_core/src/persist/user/initial_data.rs | 7 +++ rc_core/src/persist/user/mod.rs | 2 +- rc_core/src/persist/user/traits.rs | 16 +++++-- rc_database/src/schema/user_aux.rs | 1 + .../src/operations/avatar_info.rs | 44 ++++++++++++++----- rc_services_room/src/operations/avatar_set.rs | 42 ++++++++++++++++++ .../src/operations/avatar_set_custom.rs | 44 +++++++++++++++++++ rc_services_room/src/operations/mod.rs | 6 ++- .../src/operations/premium_duration.rs | 2 +- rc_social_room/src/operations/mod.rs | 1 + 13 files changed, 184 insertions(+), 34 deletions(-) create mode 100644 rc_services_room/src/operations/avatar_set.rs create mode 100644 rc_services_room/src/operations/avatar_set_custom.rs diff --git a/Cargo.lock b/Cargo.lock index 6915170..adb6042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,6 @@ dependencies = [ "flate2", "foldhash", "futures-core", - "h2", "http 0.2.12", "httparse", "httpdate", @@ -100,7 +99,6 @@ dependencies = [ "bytestring", "cfg-if", "http 0.2.12", - "regex", "regex-lite", "serde", "tracing", @@ -171,7 +169,6 @@ dependencies = [ "bytes", "bytestring", "cfg-if", - "cookie 0.16.2", "derive_more 2.0.1", "encoding_rs", "foldhash", @@ -184,7 +181,6 @@ dependencies = [ "mime", "once_cell", "pin-project-lite", - "regex", "regex-lite", "serde", "serde_json", @@ -841,17 +837,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "cookie" version = "0.18.1" @@ -3471,7 +3456,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9" dependencies = [ - "cookie 0.18.1", + "cookie", "either", "futures", "http 0.2.12", diff --git a/Cargo.toml b/Cargo.toml index 2366a92..1de3626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ [workspace.dependencies] rocket = { version = "0.5.1", features = [ "json" ] } -actix-web = { version = "4" } +actix-web = { version = "4", default-features = false, features = [ "macros", "compress-brotli", "compress-gzip", "compress-zstd"] } actix-files = "0.6" libfj = { version = "0.7.5", path = "../libfj" } log = "0.4" diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 040d6ae..ad5a958 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -526,6 +526,40 @@ impl super::User for UserData { Ok(0) } } + + async fn get_avatar_info(&self) -> Result, i16> { + let avatar_id_aux = self.db.user_aux_by_user_id_and_descriptor(self.account.id, rc_database::schema::user_aux::Descriptor::AvatarId).await + .map_err(|e| { + log::error!("Failed to retrieve AvatarId (user_aux) for user_id {}: {}", self.account.id, e); + DATABASE_ERR + })? + .ok_or_else(|| { + log::error!("Failed to find AvatarId (user_aux) for user_id {}", self.account.id); + DATABASE_ERR + })?; + let avatar_id: u32 = avatar_id_aux.data.parse() + .map_err(|e| { + log::error!("Failed to parse AvatarId (user_aux) for user_id {}: {}", self.account.id, e); + crate::data::error_codes::WebServicesError::UnexpectedError as i16 + })?; + Ok(super::GetAvatarInfo { + avatar_id: polariton::operation::Typed::Int(if avatar_id == u32::MAX { 0 } else { avatar_id as i32 }), + use_custom: polariton::operation::Typed::Bool(avatar_id == u32::MAX), + }) + } + + async fn set_avatar_info(&self, info: super::AvatarInfo) -> Result<(), i16> { + let to_update = rc_database::schema::user_aux::ActiveModel { + data: rc_database::sea_orm::ActiveValue::Set(if info.use_custom { u32::MAX } else { info.avatar_id as u32 }.to_string()), + ..Default::default() + }; + self.db.update_user_aux_by_user_id_and_descriptor(to_update, self.account.id, rc_database::schema::user_aux::Descriptor::AvatarId).await + .map_err(|e| { + log::error!("Failed to update AvatarId (user_aux) for user_id {}: {}", self.account.id, e); + DATABASE_ERR + })?; + Ok(()) + } } #[async_trait::async_trait] diff --git a/rc_core/src/persist/user/initial_data.rs b/rc_core/src/persist/user/initial_data.rs index e36fe9b..81a0f57 100644 --- a/rc_core/src/persist/user/initial_data.rs +++ b/rc_core/src/persist/user/initial_data.rs @@ -137,6 +137,13 @@ r#"{ creation_time: rc_database::sea_orm::ActiveValue::Set(current_time), descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::SubscribedChannels), data: rc_database::sea_orm::ActiveValue::Set("[\"sys\"]".to_owned()), + }, + rc_database::schema::user_aux::ActiveModel { + id: Default::default(), + user_id: rc_database::sea_orm::ActiveValue::Set(user_id), + creation_time: rc_database::sea_orm::ActiveValue::Set(current_time), + descriptor: rc_database::sea_orm::ActiveValue::Set(rc_database::schema::user_aux::Descriptor::AvatarId), + data: rc_database::sea_orm::ActiveValue::Set((current_time % 16).to_string()), } ] } diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index cbdc21e..42d115f 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -11,7 +11,7 @@ mod inventory; pub use inventory::UnlockedParts; mod traits; -pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser}; +pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo}; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 517d579..565e366 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -73,6 +73,8 @@ pub trait User: ChatUser { async fn singleplayer_robots(&self) -> Result, i16>; async fn prepare_factory_upload(&self, vehicle: VehicleUploadData) -> Result; async fn last_seen(&self) -> Result; + async fn get_avatar_info(&self) -> Result, i16>; + async fn set_avatar_info(&self, info: AvatarInfo) -> Result<(), i16>; } pub struct UserSlots { @@ -122,13 +124,21 @@ pub struct VehicleUploadData { pub thumbnail: Vec, } -use polariton::operation::Typed; +pub struct GetAvatarInfo { + pub avatar_id: polariton::operation::Typed, + pub use_custom: polariton::operation::Typed, +} + +pub struct AvatarInfo { + pub avatar_id: i32, + pub use_custom: bool, +} #[async_trait::async_trait] pub trait ChatUser { - async fn subscribed_channels(&self) -> Result, i16>; + async fn subscribed_channels(&self) -> Result, i16>; async fn subscribed_channels_strings(&self) -> Result, i16>; - async fn add_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Result, i16>; + async fn add_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Result, i16>; async fn remove_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Result<(), i16>; } diff --git a/rc_database/src/schema/user_aux.rs b/rc_database/src/schema/user_aux.rs index e8b7daa..2e397b4 100644 --- a/rc_database/src/schema/user_aux.rs +++ b/rc_database/src/schema/user_aux.rs @@ -42,4 +42,5 @@ pub enum Descriptor { GarageSlotOrder, // Vec, CSV LastSeen, // u64, seconds since Unix epoch SubscribedChannels, // Vec, JSON + AvatarId, // u32, u32::MAX means custom avatar } diff --git a/rc_services_room/src/operations/avatar_info.rs b/rc_services_room/src/operations/avatar_info.rs index 0d5dd99..e176d75 100644 --- a/rc_services_room/src/operations/avatar_info.rs +++ b/rc_services_room/src/operations/avatar_info.rs @@ -1,14 +1,36 @@ -use polariton_server::operations::SimpleFunc; -use polariton::operation::{ParameterTable, Typed}; +use polariton::operation::{ParameterTable, OperationResponse}; -const IS_CUSTOM_PARAM_KEY: u8 = 130; -const AVATAR_ID_PARAM_KEY: u8 = 129; +const CODE: u8 = 110; -pub(super) fn get_avatar_provider() -> SimpleFunc<110, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(IS_CUSTOM_PARAM_KEY, Typed::Bool(false.into())); - params.insert(AVATAR_ID_PARAM_KEY, Typed::Int(1)); - Ok(params.into()) - }) +const IS_CUSTOM_PARAM_KEY: u8 = 130; // bool +const AVATAR_ID_PARAM_KEY: u8 = 129; // int + +pub(super) fn avatar_get_provider() -> AvatarGetProvider { + AvatarGetProvider +} + +async fn do_save(params: ParameterTable<()>, user: &crate::UserTy) -> Result { + let mut params = params.to_dict(); + let user_info = user.user()?; + let info = user_info.get_avatar_info().await?; + params.insert(IS_CUSTOM_PARAM_KEY, info.use_custom); + params.insert(AVATAR_ID_PARAM_KEY, info.avatar_id); + Ok(params.into()) +} + +pub(super) struct AvatarGetProvider; + +#[async_trait::async_trait] +impl polariton_server::operations::Operation<()> for AvatarGetProvider { + type User = crate::UserTy; + + async fn handle_async(&self, params: ParameterTable<()>, user: &Self::User) -> OperationResponse<()> { + polariton_server::operations::result_to_op_resp::(do_save(params, user).await) + } +} + +impl polariton_server::operations::OperationCode for AvatarGetProvider { + fn op_code() -> u8 { + CODE + } } diff --git a/rc_services_room/src/operations/avatar_set.rs b/rc_services_room/src/operations/avatar_set.rs new file mode 100644 index 0000000..2261755 --- /dev/null +++ b/rc_services_room/src/operations/avatar_set.rs @@ -0,0 +1,42 @@ +use polariton::operation::{ParameterTable, Typed, OperationResponse}; + +const CODE: u8 = 111; + +const IS_CUSTOM_PARAM_KEY: u8 = 130; // bool; in +const AVATAR_ID_PARAM_KEY: u8 = 129; // int; in + +pub(super) fn avatar_set_provider() -> AvatarSetProvider { + AvatarSetProvider +} + +async fn do_save(params: ParameterTable<()>, user: &crate::UserTy) -> Result { + let mut params = params.to_dict(); + let user_info = user.user()?; + if let Some(Typed::Int(avatar)) = params.remove(&AVATAR_ID_PARAM_KEY) { + if let Some(Typed::Bool(is_custom)) = params.remove(&IS_CUSTOM_PARAM_KEY) { + let info = rc_core::persist::user::AvatarInfo { + avatar_id: avatar, + use_custom: is_custom, + }; + user_info.set_avatar_info(info).await?; + } + } + Ok(params.into()) +} + +pub(super) struct AvatarSetProvider; + +#[async_trait::async_trait] +impl polariton_server::operations::Operation<()> for AvatarSetProvider { + type User = crate::UserTy; + + async fn handle_async(&self, params: ParameterTable<()>, user: &Self::User) -> OperationResponse<()> { + polariton_server::operations::result_to_op_resp::(do_save(params, user).await) + } +} + +impl polariton_server::operations::OperationCode for AvatarSetProvider { + fn op_code() -> u8 { + CODE + } +} diff --git a/rc_services_room/src/operations/avatar_set_custom.rs b/rc_services_room/src/operations/avatar_set_custom.rs new file mode 100644 index 0000000..4cf56f1 --- /dev/null +++ b/rc_services_room/src/operations/avatar_set_custom.rs @@ -0,0 +1,44 @@ +use polariton::operation::{ParameterTable, Typed, OperationResponse}; + +const CODE: u8 = 112; + +const IMG_PARAM_KEY: u8 = 131; // int; in +const FORMAT_PARAM_KEY: u8 = 132; // int enum; in + +pub(super) fn custom_avatar_upload_handler() -> CustomAvatarHandler { + CustomAvatarHandler +} + +async fn do_save(params: ParameterTable<()>, _user: &crate::UserTy) -> Result { + let mut params = params.to_dict(); + //let user_info = user.user()?; + if let Some(Typed::Bytes(image)) = params.remove(&IMG_PARAM_KEY) { + if let Some(Typed::Int(format)) = params.remove(&FORMAT_PARAM_KEY) { + log::debug!("Got custom avatar ({}B) with format {}", image.vec.len(), format); + // TODO actually save image + /*let info = rc_core::persist::user::AvatarInfo { + avatar_id: 0, + use_custom: true, + }; + user_info.set_avatar_info(info).await?;*/ + } + } + Ok(params.into()) +} + +pub(super) struct CustomAvatarHandler; + +#[async_trait::async_trait] +impl polariton_server::operations::Operation<()> for CustomAvatarHandler { + type User = crate::UserTy; + + async fn handle_async(&self, params: ParameterTable<()>, user: &Self::User) -> OperationResponse<()> { + polariton_server::operations::result_to_op_resp::(do_save(params, user).await) + } +} + +impl polariton_server::operations::OperationCode for CustomAvatarHandler { + fn op_code() -> u8 { + CODE + } +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 3eba0b8..50ce836 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -91,6 +91,8 @@ mod crf_list_query; mod crf_vehicle_data; mod crf_purchase; mod crf_upload; +mod avatar_set_custom; +mod avatar_set; use polariton_server::operations::OperationsHandler; @@ -136,7 +138,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(owned_cosmetics::selected_cosmetics_provider()) .add(dev_message::dev_message_provider(&init_ctx.cubes)) .add(custom_games_maps::allowed_maps_provider()) - .add(avatar_info::get_avatar_provider()) + .add(avatar_info::avatar_get_provider()) .add(custom_game_session::get_custom_session_provider()) .add(user_xp::get_user_xp_provider()) .add(garage_upgrades::garage_upgrades_provider(&init_ctx.cubes)) @@ -204,4 +206,6 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(crf_vehicle_data::crf_item_data_provider(&init_ctx.factory)) .add(crf_purchase::crf_copy_to_bay_provider(&init_ctx.factory, init_ctx.parsers.weapon_order())) .add(crf_upload::crf_upload_provider(&init_ctx.factory)) + .add(avatar_set_custom::custom_avatar_upload_handler()) + .add(avatar_set::avatar_set_provider()) } diff --git a/rc_services_room/src/operations/premium_duration.rs b/rc_services_room/src/operations/premium_duration.rs index a2ff27c..e4eac45 100644 --- a/rc_services_room/src/operations/premium_duration.rs +++ b/rc_services_room/src/operations/premium_duration.rs @@ -14,7 +14,7 @@ pub(super) fn premium_remaining_provider() -> SimpleFunc<15, crate::UserTy, impl params.insert(HOURS_PARAM_KEY, Typed::Int(0)); params.insert(MINUTES_PARAM_KEY, Typed::Int(0)); params.insert(SECONDS_PARAM_KEY, Typed::Int(0)); - params.insert(LIFETIME_PARAM_KEY, Typed::Bool(false.into())); + params.insert(LIFETIME_PARAM_KEY, Typed::Bool(false)); Ok(params.into()) }) } diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index bac5a8d..c10f496 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -26,4 +26,5 @@ pub fn handler() -> OperationsHandler::default()) // AvatarUpdatedRequest, sent on services_room avatar_set success (just needs to be ack-ed; no params) }