avatar fixes and implementation to edit
This commit is contained in:
parent
acab2f017d
commit
c3320ddcce
11 changed files with 1417 additions and 37 deletions
|
|
@ -1968,3 +1968,127 @@ impl AvatarWithPaths {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Avatar Slot Update Models
|
||||
// =============================================================================
|
||||
|
||||
/// Request to assign an inventory item to an avatar slot.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AssignSlotRequest {
|
||||
/// Inventory item ID to assign to the slot.
|
||||
pub inventory_item_id: Uuid,
|
||||
/// Layer type: "skin", "clothes", "accessories", or an emotion name.
|
||||
pub layer: String,
|
||||
/// Grid position (0-8).
|
||||
pub position: u8,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl AssignSlotRequest {
|
||||
/// Validate the assign slot request.
|
||||
pub fn validate(&self) -> Result<(), AppError> {
|
||||
if self.position > 8 {
|
||||
return Err(AppError::Validation(
|
||||
"Position must be between 0 and 8".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate layer name
|
||||
let valid_layers = [
|
||||
"skin",
|
||||
"clothes",
|
||||
"accessories",
|
||||
"neutral",
|
||||
"happy",
|
||||
"sad",
|
||||
"angry",
|
||||
"surprised",
|
||||
"thinking",
|
||||
"laughing",
|
||||
"crying",
|
||||
"love",
|
||||
"confused",
|
||||
"sleeping",
|
||||
"wink",
|
||||
];
|
||||
if !valid_layers.contains(&self.layer.to_lowercase().as_str()) {
|
||||
return Err(AppError::Validation(format!(
|
||||
"Invalid layer: {}",
|
||||
self.layer
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the database column name for this slot.
|
||||
pub fn column_name(&self) -> String {
|
||||
let layer = self.layer.to_lowercase();
|
||||
match layer.as_str() {
|
||||
"skin" => format!("l_skin_{}", self.position),
|
||||
"clothes" => format!("l_clothes_{}", self.position),
|
||||
"accessories" => format!("l_accessories_{}", self.position),
|
||||
_ => format!("e_{}_{}", layer, self.position),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to clear an avatar slot.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClearSlotRequest {
|
||||
/// Layer type: "skin", "clothes", "accessories", or an emotion name.
|
||||
pub layer: String,
|
||||
/// Grid position (0-8).
|
||||
pub position: u8,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl ClearSlotRequest {
|
||||
/// Validate the clear slot request.
|
||||
pub fn validate(&self) -> Result<(), AppError> {
|
||||
if self.position > 8 {
|
||||
return Err(AppError::Validation(
|
||||
"Position must be between 0 and 8".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate layer name
|
||||
let valid_layers = [
|
||||
"skin",
|
||||
"clothes",
|
||||
"accessories",
|
||||
"neutral",
|
||||
"happy",
|
||||
"sad",
|
||||
"angry",
|
||||
"surprised",
|
||||
"thinking",
|
||||
"laughing",
|
||||
"crying",
|
||||
"love",
|
||||
"confused",
|
||||
"sleeping",
|
||||
"wink",
|
||||
];
|
||||
if !valid_layers.contains(&self.layer.to_lowercase().as_str()) {
|
||||
return Err(AppError::Validation(format!(
|
||||
"Invalid layer: {}",
|
||||
self.layer
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the database column name for this slot.
|
||||
pub fn column_name(&self) -> String {
|
||||
let layer = self.layer.to_lowercase();
|
||||
match layer.as_str() {
|
||||
"skin" => format!("l_skin_{}", self.position),
|
||||
"clothes" => format!("l_clothes_{}", self.position),
|
||||
"accessories" => format!("l_accessories_{}", self.position),
|
||||
_ => format!("e_{}_{}", layer, self.position),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -960,3 +960,82 @@ pub async fn set_emotion_simple<'e>(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update an avatar slot by assigning an inventory item to it.
|
||||
///
|
||||
/// The column_name should be one of:
|
||||
/// - "l_skin_0" through "l_skin_8"
|
||||
/// - "l_clothes_0" through "l_clothes_8"
|
||||
/// - "l_accessories_0" through "l_accessories_8"
|
||||
/// - "e_{emotion}_0" through "e_{emotion}_8" (e.g., "e_happy_4")
|
||||
pub async fn update_avatar_slot(
|
||||
conn: &mut PgConnection,
|
||||
user_id: Uuid,
|
||||
realm_id: Uuid,
|
||||
column_name: &str,
|
||||
inventory_id: Option<Uuid>,
|
||||
) -> Result<(), AppError> {
|
||||
// Validate column name format to prevent SQL injection
|
||||
let valid_prefixes = [
|
||||
"l_skin_",
|
||||
"l_clothes_",
|
||||
"l_accessories_",
|
||||
"e_neutral_",
|
||||
"e_happy_",
|
||||
"e_sad_",
|
||||
"e_angry_",
|
||||
"e_surprised_",
|
||||
"e_thinking_",
|
||||
"e_laughing_",
|
||||
"e_crying_",
|
||||
"e_love_",
|
||||
"e_confused_",
|
||||
"e_sleeping_",
|
||||
"e_wink_",
|
||||
];
|
||||
|
||||
let is_valid = valid_prefixes
|
||||
.iter()
|
||||
.any(|prefix| column_name.starts_with(prefix))
|
||||
&& column_name
|
||||
.chars()
|
||||
.last()
|
||||
.map(|c| c.is_ascii_digit() && c <= '8')
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_valid {
|
||||
return Err(AppError::Validation(format!(
|
||||
"Invalid column name: {}",
|
||||
column_name
|
||||
)));
|
||||
}
|
||||
|
||||
// Build dynamic UPDATE query
|
||||
// Note: We've validated the column name format above to prevent SQL injection
|
||||
let query = format!(
|
||||
r#"
|
||||
UPDATE auth.avatars
|
||||
SET {} = $3, updated_at = now()
|
||||
WHERE id = (
|
||||
SELECT avatar_id FROM auth.active_avatars
|
||||
WHERE user_id = $1 AND realm_id = $2
|
||||
)
|
||||
"#,
|
||||
column_name
|
||||
);
|
||||
|
||||
let result = sqlx::query(&query)
|
||||
.bind(user_id)
|
||||
.bind(realm_id)
|
||||
.bind(inventory_id)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound(
|
||||
"No active avatar for this user in this realm".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{ChannelMemberInfo, ChannelMemberWithAvatar, LooseProp};
|
||||
use crate::models::{AvatarRenderData, ChannelMemberInfo, ChannelMemberWithAvatar, LooseProp};
|
||||
|
||||
/// Client-to-server WebSocket messages.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -45,6 +45,9 @@ pub enum ClientMessage {
|
|||
/// Loose prop ID to pick up.
|
||||
loose_prop_id: Uuid,
|
||||
},
|
||||
|
||||
/// Request to broadcast avatar appearance to other users.
|
||||
SyncAvatar,
|
||||
}
|
||||
|
||||
/// Server-to-client WebSocket messages.
|
||||
|
|
@ -157,4 +160,14 @@ pub enum ServerMessage {
|
|||
/// ID of the expired prop.
|
||||
prop_id: Uuid,
|
||||
},
|
||||
|
||||
/// A member updated their avatar appearance.
|
||||
AvatarUpdated {
|
||||
/// User ID (if authenticated user).
|
||||
user_id: Option<Uuid>,
|
||||
/// Guest session ID (if guest).
|
||||
guest_session_id: Option<Uuid>,
|
||||
/// Updated avatar render data.
|
||||
avatar: AvatarRenderData,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue