//! Realm avatar queries. //! //! Realm avatars are pre-configured avatar configurations specific to a realm. //! They reference realm.props directly (not inventory items). use std::collections::HashMap; use chrono::{Duration, Utc}; use sqlx::PgExecutor; use uuid::Uuid; use crate::models::{AvatarRenderData, EmotionState, RealmAvatar}; use chattyness_error::AppError; /// Get a realm avatar by slug within a realm. pub async fn get_realm_avatar_by_slug<'e>( executor: impl PgExecutor<'e>, realm_id: Uuid, slug: &str, ) -> Result, AppError> { let avatar = sqlx::query_as::<_, RealmAvatar>( r#" SELECT * FROM realm.avatars WHERE realm_id = $1 AND slug = $2 AND is_active = true "#, ) .bind(realm_id) .bind(slug) .fetch_optional(executor) .await?; Ok(avatar) } /// Get a realm avatar by ID. pub async fn get_realm_avatar_by_id<'e>( executor: impl PgExecutor<'e>, avatar_id: Uuid, ) -> Result, AppError> { let avatar = sqlx::query_as::<_, RealmAvatar>( r#" SELECT * FROM realm.avatars WHERE id = $1 "#, ) .bind(avatar_id) .fetch_optional(executor) .await?; Ok(avatar) } /// List all active public realm avatars for a realm. pub async fn list_public_realm_avatars<'e>( executor: impl PgExecutor<'e>, realm_id: Uuid, ) -> Result, AppError> { let avatars = sqlx::query_as::<_, RealmAvatar>( r#" SELECT * FROM realm.avatars WHERE realm_id = $1 AND is_active = true AND is_public = true ORDER BY name ASC "#, ) .bind(realm_id) .fetch_all(executor) .await?; Ok(avatars) } use crate::models::RealmAvatarWithPaths; /// Row type for realm avatar with paths query. #[derive(Debug, sqlx::FromRow)] struct RealmAvatarWithPathsRow { id: Uuid, name: String, description: Option, // Skin layer paths skin_0: Option, skin_1: Option, skin_2: Option, skin_3: Option, skin_4: Option, skin_5: Option, skin_6: Option, skin_7: Option, skin_8: Option, // Clothes layer paths clothes_0: Option, clothes_1: Option, clothes_2: Option, clothes_3: Option, clothes_4: Option, clothes_5: Option, clothes_6: Option, clothes_7: Option, clothes_8: Option, // Accessories layer paths accessories_0: Option, accessories_1: Option, accessories_2: Option, accessories_3: Option, accessories_4: Option, accessories_5: Option, accessories_6: Option, accessories_7: Option, accessories_8: Option, // Happy emotion layer paths (e1 - more inviting for store display) emotion_0: Option, emotion_1: Option, emotion_2: Option, emotion_3: Option, emotion_4: Option, emotion_5: Option, emotion_6: Option, emotion_7: Option, emotion_8: Option, } impl From for RealmAvatarWithPaths { fn from(row: RealmAvatarWithPathsRow) -> Self { Self { id: row.id, name: row.name, description: row.description, skin_layer: [ row.skin_0, row.skin_1, row.skin_2, row.skin_3, row.skin_4, row.skin_5, row.skin_6, row.skin_7, row.skin_8, ], clothes_layer: [ row.clothes_0, row.clothes_1, row.clothes_2, row.clothes_3, row.clothes_4, row.clothes_5, row.clothes_6, row.clothes_7, row.clothes_8, ], accessories_layer: [ row.accessories_0, row.accessories_1, row.accessories_2, row.accessories_3, row.accessories_4, row.accessories_5, row.accessories_6, row.accessories_7, row.accessories_8, ], emotion_layer: [ row.emotion_0, row.emotion_1, row.emotion_2, row.emotion_3, row.emotion_4, row.emotion_5, row.emotion_6, row.emotion_7, row.emotion_8, ], } } } /// List all active public realm avatars with resolved asset paths. /// /// Joins with the props table to resolve prop UUIDs to asset paths, /// suitable for client-side rendering without additional lookups. pub async fn list_public_realm_avatars_with_paths<'e>( executor: impl PgExecutor<'e>, realm_id: Uuid, ) -> Result, AppError> { let rows = sqlx::query_as::<_, RealmAvatarWithPathsRow>( r#" SELECT a.id, a.name, a.description, -- Skin layer p_skin_0.asset_path AS skin_0, p_skin_1.asset_path AS skin_1, p_skin_2.asset_path AS skin_2, p_skin_3.asset_path AS skin_3, p_skin_4.asset_path AS skin_4, p_skin_5.asset_path AS skin_5, p_skin_6.asset_path AS skin_6, p_skin_7.asset_path AS skin_7, p_skin_8.asset_path AS skin_8, -- Clothes layer p_clothes_0.asset_path AS clothes_0, p_clothes_1.asset_path AS clothes_1, p_clothes_2.asset_path AS clothes_2, p_clothes_3.asset_path AS clothes_3, p_clothes_4.asset_path AS clothes_4, p_clothes_5.asset_path AS clothes_5, p_clothes_6.asset_path AS clothes_6, p_clothes_7.asset_path AS clothes_7, p_clothes_8.asset_path AS clothes_8, -- Accessories layer p_acc_0.asset_path AS accessories_0, p_acc_1.asset_path AS accessories_1, p_acc_2.asset_path AS accessories_2, p_acc_3.asset_path AS accessories_3, p_acc_4.asset_path AS accessories_4, p_acc_5.asset_path AS accessories_5, p_acc_6.asset_path AS accessories_6, p_acc_7.asset_path AS accessories_7, p_acc_8.asset_path AS accessories_8, -- Happy emotion layer (e1 - more inviting for store display) p_emo_0.asset_path AS emotion_0, p_emo_1.asset_path AS emotion_1, p_emo_2.asset_path AS emotion_2, p_emo_3.asset_path AS emotion_3, p_emo_4.asset_path AS emotion_4, p_emo_5.asset_path AS emotion_5, p_emo_6.asset_path AS emotion_6, p_emo_7.asset_path AS emotion_7, p_emo_8.asset_path AS emotion_8 FROM realm.avatars a -- Skin layer joins LEFT JOIN realm.props p_skin_0 ON a.l_skin_0 = p_skin_0.id LEFT JOIN realm.props p_skin_1 ON a.l_skin_1 = p_skin_1.id LEFT JOIN realm.props p_skin_2 ON a.l_skin_2 = p_skin_2.id LEFT JOIN realm.props p_skin_3 ON a.l_skin_3 = p_skin_3.id LEFT JOIN realm.props p_skin_4 ON a.l_skin_4 = p_skin_4.id LEFT JOIN realm.props p_skin_5 ON a.l_skin_5 = p_skin_5.id LEFT JOIN realm.props p_skin_6 ON a.l_skin_6 = p_skin_6.id LEFT JOIN realm.props p_skin_7 ON a.l_skin_7 = p_skin_7.id LEFT JOIN realm.props p_skin_8 ON a.l_skin_8 = p_skin_8.id -- Clothes layer joins LEFT JOIN realm.props p_clothes_0 ON a.l_clothes_0 = p_clothes_0.id LEFT JOIN realm.props p_clothes_1 ON a.l_clothes_1 = p_clothes_1.id LEFT JOIN realm.props p_clothes_2 ON a.l_clothes_2 = p_clothes_2.id LEFT JOIN realm.props p_clothes_3 ON a.l_clothes_3 = p_clothes_3.id LEFT JOIN realm.props p_clothes_4 ON a.l_clothes_4 = p_clothes_4.id LEFT JOIN realm.props p_clothes_5 ON a.l_clothes_5 = p_clothes_5.id LEFT JOIN realm.props p_clothes_6 ON a.l_clothes_6 = p_clothes_6.id LEFT JOIN realm.props p_clothes_7 ON a.l_clothes_7 = p_clothes_7.id LEFT JOIN realm.props p_clothes_8 ON a.l_clothes_8 = p_clothes_8.id -- Accessories layer joins LEFT JOIN realm.props p_acc_0 ON a.l_accessories_0 = p_acc_0.id LEFT JOIN realm.props p_acc_1 ON a.l_accessories_1 = p_acc_1.id LEFT JOIN realm.props p_acc_2 ON a.l_accessories_2 = p_acc_2.id LEFT JOIN realm.props p_acc_3 ON a.l_accessories_3 = p_acc_3.id LEFT JOIN realm.props p_acc_4 ON a.l_accessories_4 = p_acc_4.id LEFT JOIN realm.props p_acc_5 ON a.l_accessories_5 = p_acc_5.id LEFT JOIN realm.props p_acc_6 ON a.l_accessories_6 = p_acc_6.id LEFT JOIN realm.props p_acc_7 ON a.l_accessories_7 = p_acc_7.id LEFT JOIN realm.props p_acc_8 ON a.l_accessories_8 = p_acc_8.id -- Happy emotion layer joins (e1 - more inviting for store display) LEFT JOIN realm.props p_emo_0 ON a.e_happy_0 = p_emo_0.id LEFT JOIN realm.props p_emo_1 ON a.e_happy_1 = p_emo_1.id LEFT JOIN realm.props p_emo_2 ON a.e_happy_2 = p_emo_2.id LEFT JOIN realm.props p_emo_3 ON a.e_happy_3 = p_emo_3.id LEFT JOIN realm.props p_emo_4 ON a.e_happy_4 = p_emo_4.id LEFT JOIN realm.props p_emo_5 ON a.e_happy_5 = p_emo_5.id LEFT JOIN realm.props p_emo_6 ON a.e_happy_6 = p_emo_6.id LEFT JOIN realm.props p_emo_7 ON a.e_happy_7 = p_emo_7.id LEFT JOIN realm.props p_emo_8 ON a.e_happy_8 = p_emo_8.id WHERE a.realm_id = $1 AND a.is_active = true AND a.is_public = true ORDER BY a.name ASC "#, ) .bind(realm_id) .fetch_all(executor) .await?; Ok(rows.into_iter().map(RealmAvatarWithPaths::from).collect()) } /// Row type for prop asset lookup. #[derive(Debug, sqlx::FromRow)] struct PropAssetRow { id: Uuid, asset_path: String, } /// Resolve a realm avatar to render data. /// Joins the avatar's prop UUIDs with realm.props to get asset paths. pub async fn resolve_realm_avatar_to_render_data<'e>( executor: impl PgExecutor<'e>, avatar: &RealmAvatar, current_emotion: EmotionState, ) -> Result { // Collect all non-null prop UUIDs let mut prop_ids: Vec = Vec::new(); // Content layers for id in [ avatar.l_skin_0, avatar.l_skin_1, avatar.l_skin_2, avatar.l_skin_3, avatar.l_skin_4, avatar.l_skin_5, avatar.l_skin_6, avatar.l_skin_7, avatar.l_skin_8, avatar.l_clothes_0, avatar.l_clothes_1, avatar.l_clothes_2, avatar.l_clothes_3, avatar.l_clothes_4, avatar.l_clothes_5, avatar.l_clothes_6, avatar.l_clothes_7, avatar.l_clothes_8, avatar.l_accessories_0, avatar.l_accessories_1, avatar.l_accessories_2, avatar.l_accessories_3, avatar.l_accessories_4, avatar.l_accessories_5, avatar.l_accessories_6, avatar.l_accessories_7, avatar.l_accessories_8, ].iter().flatten() { prop_ids.push(*id); } // Get emotion layer slots based on current emotion let emotion_slots: [Option; 9] = match current_emotion { EmotionState::Neutral => [avatar.e_neutral_0, avatar.e_neutral_1, avatar.e_neutral_2, avatar.e_neutral_3, avatar.e_neutral_4, avatar.e_neutral_5, avatar.e_neutral_6, avatar.e_neutral_7, avatar.e_neutral_8], EmotionState::Happy => [avatar.e_happy_0, avatar.e_happy_1, avatar.e_happy_2, avatar.e_happy_3, avatar.e_happy_4, avatar.e_happy_5, avatar.e_happy_6, avatar.e_happy_7, avatar.e_happy_8], EmotionState::Sad => [avatar.e_sad_0, avatar.e_sad_1, avatar.e_sad_2, avatar.e_sad_3, avatar.e_sad_4, avatar.e_sad_5, avatar.e_sad_6, avatar.e_sad_7, avatar.e_sad_8], EmotionState::Angry => [avatar.e_angry_0, avatar.e_angry_1, avatar.e_angry_2, avatar.e_angry_3, avatar.e_angry_4, avatar.e_angry_5, avatar.e_angry_6, avatar.e_angry_7, avatar.e_angry_8], EmotionState::Surprised => [avatar.e_surprised_0, avatar.e_surprised_1, avatar.e_surprised_2, avatar.e_surprised_3, avatar.e_surprised_4, avatar.e_surprised_5, avatar.e_surprised_6, avatar.e_surprised_7, avatar.e_surprised_8], EmotionState::Thinking => [avatar.e_thinking_0, avatar.e_thinking_1, avatar.e_thinking_2, avatar.e_thinking_3, avatar.e_thinking_4, avatar.e_thinking_5, avatar.e_thinking_6, avatar.e_thinking_7, avatar.e_thinking_8], EmotionState::Laughing => [avatar.e_laughing_0, avatar.e_laughing_1, avatar.e_laughing_2, avatar.e_laughing_3, avatar.e_laughing_4, avatar.e_laughing_5, avatar.e_laughing_6, avatar.e_laughing_7, avatar.e_laughing_8], EmotionState::Crying => [avatar.e_crying_0, avatar.e_crying_1, avatar.e_crying_2, avatar.e_crying_3, avatar.e_crying_4, avatar.e_crying_5, avatar.e_crying_6, avatar.e_crying_7, avatar.e_crying_8], EmotionState::Love => [avatar.e_love_0, avatar.e_love_1, avatar.e_love_2, avatar.e_love_3, avatar.e_love_4, avatar.e_love_5, avatar.e_love_6, avatar.e_love_7, avatar.e_love_8], EmotionState::Confused => [avatar.e_confused_0, avatar.e_confused_1, avatar.e_confused_2, avatar.e_confused_3, avatar.e_confused_4, avatar.e_confused_5, avatar.e_confused_6, avatar.e_confused_7, avatar.e_confused_8], EmotionState::Sleeping => [avatar.e_sleeping_0, avatar.e_sleeping_1, avatar.e_sleeping_2, avatar.e_sleeping_3, avatar.e_sleeping_4, avatar.e_sleeping_5, avatar.e_sleeping_6, avatar.e_sleeping_7, avatar.e_sleeping_8], EmotionState::Wink => [avatar.e_wink_0, avatar.e_wink_1, avatar.e_wink_2, avatar.e_wink_3, avatar.e_wink_4, avatar.e_wink_5, avatar.e_wink_6, avatar.e_wink_7, avatar.e_wink_8], }; for id in emotion_slots.iter().flatten() { prop_ids.push(*id); } // Bulk lookup all prop asset paths from realm.props let prop_map: HashMap = if prop_ids.is_empty() { HashMap::new() } else { let rows = sqlx::query_as::<_, PropAssetRow>( r#" SELECT id, asset_path FROM realm.props WHERE id = ANY($1) "#, ) .bind(&prop_ids) .fetch_all(executor) .await?; rows.into_iter().map(|r| (r.id, r.asset_path)).collect() }; // Helper to look up path let get_path = |id: Option| -> Option { id.and_then(|id| prop_map.get(&id).cloned()) }; Ok(AvatarRenderData { avatar_id: avatar.id, current_emotion, skin_layer: [ get_path(avatar.l_skin_0), get_path(avatar.l_skin_1), get_path(avatar.l_skin_2), get_path(avatar.l_skin_3), get_path(avatar.l_skin_4), get_path(avatar.l_skin_5), get_path(avatar.l_skin_6), get_path(avatar.l_skin_7), get_path(avatar.l_skin_8), ], clothes_layer: [ get_path(avatar.l_clothes_0), get_path(avatar.l_clothes_1), get_path(avatar.l_clothes_2), get_path(avatar.l_clothes_3), get_path(avatar.l_clothes_4), get_path(avatar.l_clothes_5), get_path(avatar.l_clothes_6), get_path(avatar.l_clothes_7), get_path(avatar.l_clothes_8), ], accessories_layer: [ get_path(avatar.l_accessories_0), get_path(avatar.l_accessories_1), get_path(avatar.l_accessories_2), get_path(avatar.l_accessories_3), get_path(avatar.l_accessories_4), get_path(avatar.l_accessories_5), get_path(avatar.l_accessories_6), get_path(avatar.l_accessories_7), get_path(avatar.l_accessories_8), ], emotion_layer: [ get_path(emotion_slots[0]), get_path(emotion_slots[1]), get_path(emotion_slots[2]), get_path(emotion_slots[3]), get_path(emotion_slots[4]), get_path(emotion_slots[5]), get_path(emotion_slots[6]), get_path(emotion_slots[7]), get_path(emotion_slots[8]), ], }) } /// Apply a forced realm avatar to a user. pub async fn apply_forced_realm_avatar<'e>( executor: impl PgExecutor<'e>, user_id: Uuid, realm_id: Uuid, avatar_id: Uuid, forced_by: Option, duration: Option, ) -> Result<(), AppError> { let forced_until = duration.map(|d| Utc::now() + d); sqlx::query( r#" UPDATE auth.active_avatars SET forced_avatar_id = $3, forced_avatar_source = 'realm', forced_by = $4, forced_until = $5, updated_at = now() WHERE user_id = $1 AND realm_id = $2 "#, ) .bind(user_id) .bind(realm_id) .bind(avatar_id) .bind(forced_by) .bind(forced_until) .execute(executor) .await?; Ok(()) } /// Get scene forced avatar configuration. #[derive(Debug, sqlx::FromRow)] pub struct SceneForcedAvatar { pub forced_avatar_id: Uuid, pub forced_avatar_source: String, } pub async fn get_scene_forced_avatar<'e>( executor: impl PgExecutor<'e>, scene_id: Uuid, ) -> Result, AppError> { let info = sqlx::query_as::<_, SceneForcedAvatar>( r#" SELECT forced_avatar_id, forced_avatar_source FROM realm.scenes WHERE id = $1 AND forced_avatar_id IS NOT NULL "#, ) .bind(scene_id) .fetch_optional(executor) .await?; Ok(info) } /// Apply scene-forced avatar to a user's active avatar. pub async fn apply_scene_forced_avatar<'e>( executor: impl PgExecutor<'e>, user_id: Uuid, realm_id: Uuid, avatar_id: Uuid, ) -> Result<(), AppError> { sqlx::query( r#" UPDATE auth.active_avatars SET forced_avatar_id = $3, forced_avatar_source = 'scene', forced_by = NULL, forced_until = NULL, updated_at = now() WHERE user_id = $1 AND realm_id = $2 "#, ) .bind(user_id) .bind(realm_id) .bind(avatar_id) .execute(executor) .await?; Ok(()) } // ============================================================================= // CRUD Operations for Admin API // ============================================================================= use crate::models::{CreateRealmAvatarRequest, RealmAvatarSummary, UpdateRealmAvatarRequest}; /// List all realm avatars for a realm (for admin). pub async fn list_all_realm_avatars<'e>( executor: impl PgExecutor<'e>, realm_id: Uuid, ) -> Result, AppError> { let avatars = sqlx::query_as::<_, RealmAvatarSummary>( r#" SELECT id, realm_id, slug, name, description, is_public, is_active, thumbnail_path, created_at FROM realm.avatars WHERE realm_id = $1 ORDER BY name ASC "#, ) .bind(realm_id) .fetch_all(executor) .await?; Ok(avatars) } /// Check if a realm avatar slug is available within a realm. pub async fn is_avatar_slug_available<'e>( executor: impl PgExecutor<'e>, realm_id: Uuid, slug: &str, ) -> Result { let result: (bool,) = sqlx::query_as( r#" SELECT NOT EXISTS( SELECT 1 FROM realm.avatars WHERE realm_id = $1 AND slug = $2 ) "#, ) .bind(realm_id) .bind(slug) .fetch_one(executor) .await?; Ok(result.0) } /// Create a new realm avatar. pub async fn create_realm_avatar<'e>( executor: impl PgExecutor<'e>, realm_id: Uuid, req: &CreateRealmAvatarRequest, created_by: Option, ) -> Result { let slug = req.slug_or_generate(); let avatar = sqlx::query_as::<_, RealmAvatar>( r#" INSERT INTO realm.avatars ( realm_id, slug, name, description, is_public, is_active, thumbnail_path, created_by, l_skin_0, l_skin_1, l_skin_2, l_skin_3, l_skin_4, l_skin_5, l_skin_6, l_skin_7, l_skin_8, l_clothes_0, l_clothes_1, l_clothes_2, l_clothes_3, l_clothes_4, l_clothes_5, l_clothes_6, l_clothes_7, l_clothes_8, l_accessories_0, l_accessories_1, l_accessories_2, l_accessories_3, l_accessories_4, l_accessories_5, l_accessories_6, l_accessories_7, l_accessories_8, e_neutral_0, e_neutral_1, e_neutral_2, e_neutral_3, e_neutral_4, e_neutral_5, e_neutral_6, e_neutral_7, e_neutral_8, e_happy_0, e_happy_1, e_happy_2, e_happy_3, e_happy_4, e_happy_5, e_happy_6, e_happy_7, e_happy_8, e_sad_0, e_sad_1, e_sad_2, e_sad_3, e_sad_4, e_sad_5, e_sad_6, e_sad_7, e_sad_8, e_angry_0, e_angry_1, e_angry_2, e_angry_3, e_angry_4, e_angry_5, e_angry_6, e_angry_7, e_angry_8, e_surprised_0, e_surprised_1, e_surprised_2, e_surprised_3, e_surprised_4, e_surprised_5, e_surprised_6, e_surprised_7, e_surprised_8, e_thinking_0, e_thinking_1, e_thinking_2, e_thinking_3, e_thinking_4, e_thinking_5, e_thinking_6, e_thinking_7, e_thinking_8, e_laughing_0, e_laughing_1, e_laughing_2, e_laughing_3, e_laughing_4, e_laughing_5, e_laughing_6, e_laughing_7, e_laughing_8, e_crying_0, e_crying_1, e_crying_2, e_crying_3, e_crying_4, e_crying_5, e_crying_6, e_crying_7, e_crying_8, e_love_0, e_love_1, e_love_2, e_love_3, e_love_4, e_love_5, e_love_6, e_love_7, e_love_8, e_confused_0, e_confused_1, e_confused_2, e_confused_3, e_confused_4, e_confused_5, e_confused_6, e_confused_7, e_confused_8, e_sleeping_0, e_sleeping_1, e_sleeping_2, e_sleeping_3, e_sleeping_4, e_sleeping_5, e_sleeping_6, e_sleeping_7, e_sleeping_8, e_wink_0, e_wink_1, e_wink_2, e_wink_3, e_wink_4, e_wink_5, e_wink_6, e_wink_7, e_wink_8 ) VALUES ( $1, $2, $3, $4, $5, true, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57, $58, $59, $60, $61, $62, $63, $64, $65, $66, $67, $68, $69, $70, $71, $72, $73, $74, $75, $76, $77, $78, $79, $80, $81, $82, $83, $84, $85, $86, $87, $88, $89, $90, $91, $92, $93, $94, $95, $96, $97, $98, $99, $100, $101, $102, $103, $104, $105, $106, $107, $108, $109, $110, $111, $112, $113, $114, $115, $116, $117, $118, $119, $120, $121, $122, $123, $124, $125, $126, $127, $128, $129, $130, $131, $132, $133, $134, $135, $136, $137, $138, $139, $140, $141, $142 ) RETURNING * "#, ) .bind(realm_id) .bind(&slug) .bind(&req.name) .bind(&req.description) .bind(req.is_public) .bind(&req.thumbnail_path) .bind(created_by) // Skin layer .bind(req.l_skin_0) .bind(req.l_skin_1) .bind(req.l_skin_2) .bind(req.l_skin_3) .bind(req.l_skin_4) .bind(req.l_skin_5) .bind(req.l_skin_6) .bind(req.l_skin_7) .bind(req.l_skin_8) // Clothes layer .bind(req.l_clothes_0) .bind(req.l_clothes_1) .bind(req.l_clothes_2) .bind(req.l_clothes_3) .bind(req.l_clothes_4) .bind(req.l_clothes_5) .bind(req.l_clothes_6) .bind(req.l_clothes_7) .bind(req.l_clothes_8) // Accessories layer .bind(req.l_accessories_0) .bind(req.l_accessories_1) .bind(req.l_accessories_2) .bind(req.l_accessories_3) .bind(req.l_accessories_4) .bind(req.l_accessories_5) .bind(req.l_accessories_6) .bind(req.l_accessories_7) .bind(req.l_accessories_8) // Neutral emotion .bind(req.e_neutral_0) .bind(req.e_neutral_1) .bind(req.e_neutral_2) .bind(req.e_neutral_3) .bind(req.e_neutral_4) .bind(req.e_neutral_5) .bind(req.e_neutral_6) .bind(req.e_neutral_7) .bind(req.e_neutral_8) // Happy emotion .bind(req.e_happy_0) .bind(req.e_happy_1) .bind(req.e_happy_2) .bind(req.e_happy_3) .bind(req.e_happy_4) .bind(req.e_happy_5) .bind(req.e_happy_6) .bind(req.e_happy_7) .bind(req.e_happy_8) // Sad emotion .bind(req.e_sad_0) .bind(req.e_sad_1) .bind(req.e_sad_2) .bind(req.e_sad_3) .bind(req.e_sad_4) .bind(req.e_sad_5) .bind(req.e_sad_6) .bind(req.e_sad_7) .bind(req.e_sad_8) // Angry emotion .bind(req.e_angry_0) .bind(req.e_angry_1) .bind(req.e_angry_2) .bind(req.e_angry_3) .bind(req.e_angry_4) .bind(req.e_angry_5) .bind(req.e_angry_6) .bind(req.e_angry_7) .bind(req.e_angry_8) // Surprised emotion .bind(req.e_surprised_0) .bind(req.e_surprised_1) .bind(req.e_surprised_2) .bind(req.e_surprised_3) .bind(req.e_surprised_4) .bind(req.e_surprised_5) .bind(req.e_surprised_6) .bind(req.e_surprised_7) .bind(req.e_surprised_8) // Thinking emotion .bind(req.e_thinking_0) .bind(req.e_thinking_1) .bind(req.e_thinking_2) .bind(req.e_thinking_3) .bind(req.e_thinking_4) .bind(req.e_thinking_5) .bind(req.e_thinking_6) .bind(req.e_thinking_7) .bind(req.e_thinking_8) // Laughing emotion .bind(req.e_laughing_0) .bind(req.e_laughing_1) .bind(req.e_laughing_2) .bind(req.e_laughing_3) .bind(req.e_laughing_4) .bind(req.e_laughing_5) .bind(req.e_laughing_6) .bind(req.e_laughing_7) .bind(req.e_laughing_8) // Crying emotion .bind(req.e_crying_0) .bind(req.e_crying_1) .bind(req.e_crying_2) .bind(req.e_crying_3) .bind(req.e_crying_4) .bind(req.e_crying_5) .bind(req.e_crying_6) .bind(req.e_crying_7) .bind(req.e_crying_8) // Love emotion .bind(req.e_love_0) .bind(req.e_love_1) .bind(req.e_love_2) .bind(req.e_love_3) .bind(req.e_love_4) .bind(req.e_love_5) .bind(req.e_love_6) .bind(req.e_love_7) .bind(req.e_love_8) // Confused emotion .bind(req.e_confused_0) .bind(req.e_confused_1) .bind(req.e_confused_2) .bind(req.e_confused_3) .bind(req.e_confused_4) .bind(req.e_confused_5) .bind(req.e_confused_6) .bind(req.e_confused_7) .bind(req.e_confused_8) // Sleeping emotion .bind(req.e_sleeping_0) .bind(req.e_sleeping_1) .bind(req.e_sleeping_2) .bind(req.e_sleeping_3) .bind(req.e_sleeping_4) .bind(req.e_sleeping_5) .bind(req.e_sleeping_6) .bind(req.e_sleeping_7) .bind(req.e_sleeping_8) // Wink emotion .bind(req.e_wink_0) .bind(req.e_wink_1) .bind(req.e_wink_2) .bind(req.e_wink_3) .bind(req.e_wink_4) .bind(req.e_wink_5) .bind(req.e_wink_6) .bind(req.e_wink_7) .bind(req.e_wink_8) .fetch_one(executor) .await?; Ok(avatar) } /// Update a realm avatar. pub async fn update_realm_avatar<'e>( executor: impl PgExecutor<'e>, avatar_id: Uuid, req: &UpdateRealmAvatarRequest, ) -> Result { let avatar = sqlx::query_as::<_, RealmAvatar>( r#" UPDATE realm.avatars SET name = COALESCE($2, name), description = COALESCE($3, description), is_public = COALESCE($4, is_public), is_active = COALESCE($5, is_active), thumbnail_path = COALESCE($6, thumbnail_path), l_skin_0 = COALESCE($7, l_skin_0), l_skin_1 = COALESCE($8, l_skin_1), l_skin_2 = COALESCE($9, l_skin_2), l_skin_3 = COALESCE($10, l_skin_3), l_skin_4 = COALESCE($11, l_skin_4), l_skin_5 = COALESCE($12, l_skin_5), l_skin_6 = COALESCE($13, l_skin_6), l_skin_7 = COALESCE($14, l_skin_7), l_skin_8 = COALESCE($15, l_skin_8), l_clothes_0 = COALESCE($16, l_clothes_0), l_clothes_1 = COALESCE($17, l_clothes_1), l_clothes_2 = COALESCE($18, l_clothes_2), l_clothes_3 = COALESCE($19, l_clothes_3), l_clothes_4 = COALESCE($20, l_clothes_4), l_clothes_5 = COALESCE($21, l_clothes_5), l_clothes_6 = COALESCE($22, l_clothes_6), l_clothes_7 = COALESCE($23, l_clothes_7), l_clothes_8 = COALESCE($24, l_clothes_8), l_accessories_0 = COALESCE($25, l_accessories_0), l_accessories_1 = COALESCE($26, l_accessories_1), l_accessories_2 = COALESCE($27, l_accessories_2), l_accessories_3 = COALESCE($28, l_accessories_3), l_accessories_4 = COALESCE($29, l_accessories_4), l_accessories_5 = COALESCE($30, l_accessories_5), l_accessories_6 = COALESCE($31, l_accessories_6), l_accessories_7 = COALESCE($32, l_accessories_7), l_accessories_8 = COALESCE($33, l_accessories_8), e_neutral_0 = COALESCE($34, e_neutral_0), e_neutral_1 = COALESCE($35, e_neutral_1), e_neutral_2 = COALESCE($36, e_neutral_2), e_neutral_3 = COALESCE($37, e_neutral_3), e_neutral_4 = COALESCE($38, e_neutral_4), e_neutral_5 = COALESCE($39, e_neutral_5), e_neutral_6 = COALESCE($40, e_neutral_6), e_neutral_7 = COALESCE($41, e_neutral_7), e_neutral_8 = COALESCE($42, e_neutral_8), e_happy_0 = COALESCE($43, e_happy_0), e_happy_1 = COALESCE($44, e_happy_1), e_happy_2 = COALESCE($45, e_happy_2), e_happy_3 = COALESCE($46, e_happy_3), e_happy_4 = COALESCE($47, e_happy_4), e_happy_5 = COALESCE($48, e_happy_5), e_happy_6 = COALESCE($49, e_happy_6), e_happy_7 = COALESCE($50, e_happy_7), e_happy_8 = COALESCE($51, e_happy_8), e_sad_0 = COALESCE($52, e_sad_0), e_sad_1 = COALESCE($53, e_sad_1), e_sad_2 = COALESCE($54, e_sad_2), e_sad_3 = COALESCE($55, e_sad_3), e_sad_4 = COALESCE($56, e_sad_4), e_sad_5 = COALESCE($57, e_sad_5), e_sad_6 = COALESCE($58, e_sad_6), e_sad_7 = COALESCE($59, e_sad_7), e_sad_8 = COALESCE($60, e_sad_8), e_angry_0 = COALESCE($61, e_angry_0), e_angry_1 = COALESCE($62, e_angry_1), e_angry_2 = COALESCE($63, e_angry_2), e_angry_3 = COALESCE($64, e_angry_3), e_angry_4 = COALESCE($65, e_angry_4), e_angry_5 = COALESCE($66, e_angry_5), e_angry_6 = COALESCE($67, e_angry_6), e_angry_7 = COALESCE($68, e_angry_7), e_angry_8 = COALESCE($69, e_angry_8), e_surprised_0 = COALESCE($70, e_surprised_0), e_surprised_1 = COALESCE($71, e_surprised_1), e_surprised_2 = COALESCE($72, e_surprised_2), e_surprised_3 = COALESCE($73, e_surprised_3), e_surprised_4 = COALESCE($74, e_surprised_4), e_surprised_5 = COALESCE($75, e_surprised_5), e_surprised_6 = COALESCE($76, e_surprised_6), e_surprised_7 = COALESCE($77, e_surprised_7), e_surprised_8 = COALESCE($78, e_surprised_8), e_thinking_0 = COALESCE($79, e_thinking_0), e_thinking_1 = COALESCE($80, e_thinking_1), e_thinking_2 = COALESCE($81, e_thinking_2), e_thinking_3 = COALESCE($82, e_thinking_3), e_thinking_4 = COALESCE($83, e_thinking_4), e_thinking_5 = COALESCE($84, e_thinking_5), e_thinking_6 = COALESCE($85, e_thinking_6), e_thinking_7 = COALESCE($86, e_thinking_7), e_thinking_8 = COALESCE($87, e_thinking_8), e_laughing_0 = COALESCE($88, e_laughing_0), e_laughing_1 = COALESCE($89, e_laughing_1), e_laughing_2 = COALESCE($90, e_laughing_2), e_laughing_3 = COALESCE($91, e_laughing_3), e_laughing_4 = COALESCE($92, e_laughing_4), e_laughing_5 = COALESCE($93, e_laughing_5), e_laughing_6 = COALESCE($94, e_laughing_6), e_laughing_7 = COALESCE($95, e_laughing_7), e_laughing_8 = COALESCE($96, e_laughing_8), e_crying_0 = COALESCE($97, e_crying_0), e_crying_1 = COALESCE($98, e_crying_1), e_crying_2 = COALESCE($99, e_crying_2), e_crying_3 = COALESCE($100, e_crying_3), e_crying_4 = COALESCE($101, e_crying_4), e_crying_5 = COALESCE($102, e_crying_5), e_crying_6 = COALESCE($103, e_crying_6), e_crying_7 = COALESCE($104, e_crying_7), e_crying_8 = COALESCE($105, e_crying_8), e_love_0 = COALESCE($106, e_love_0), e_love_1 = COALESCE($107, e_love_1), e_love_2 = COALESCE($108, e_love_2), e_love_3 = COALESCE($109, e_love_3), e_love_4 = COALESCE($110, e_love_4), e_love_5 = COALESCE($111, e_love_5), e_love_6 = COALESCE($112, e_love_6), e_love_7 = COALESCE($113, e_love_7), e_love_8 = COALESCE($114, e_love_8), e_confused_0 = COALESCE($115, e_confused_0), e_confused_1 = COALESCE($116, e_confused_1), e_confused_2 = COALESCE($117, e_confused_2), e_confused_3 = COALESCE($118, e_confused_3), e_confused_4 = COALESCE($119, e_confused_4), e_confused_5 = COALESCE($120, e_confused_5), e_confused_6 = COALESCE($121, e_confused_6), e_confused_7 = COALESCE($122, e_confused_7), e_confused_8 = COALESCE($123, e_confused_8), e_sleeping_0 = COALESCE($124, e_sleeping_0), e_sleeping_1 = COALESCE($125, e_sleeping_1), e_sleeping_2 = COALESCE($126, e_sleeping_2), e_sleeping_3 = COALESCE($127, e_sleeping_3), e_sleeping_4 = COALESCE($128, e_sleeping_4), e_sleeping_5 = COALESCE($129, e_sleeping_5), e_sleeping_6 = COALESCE($130, e_sleeping_6), e_sleeping_7 = COALESCE($131, e_sleeping_7), e_sleeping_8 = COALESCE($132, e_sleeping_8), e_wink_0 = COALESCE($133, e_wink_0), e_wink_1 = COALESCE($134, e_wink_1), e_wink_2 = COALESCE($135, e_wink_2), e_wink_3 = COALESCE($136, e_wink_3), e_wink_4 = COALESCE($137, e_wink_4), e_wink_5 = COALESCE($138, e_wink_5), e_wink_6 = COALESCE($139, e_wink_6), e_wink_7 = COALESCE($140, e_wink_7), e_wink_8 = COALESCE($141, e_wink_8), updated_at = now() WHERE id = $1 RETURNING * "#, ) .bind(avatar_id) .bind(&req.name) .bind(&req.description) .bind(req.is_public) .bind(req.is_active) .bind(&req.thumbnail_path) // Skin layer .bind(req.l_skin_0) .bind(req.l_skin_1) .bind(req.l_skin_2) .bind(req.l_skin_3) .bind(req.l_skin_4) .bind(req.l_skin_5) .bind(req.l_skin_6) .bind(req.l_skin_7) .bind(req.l_skin_8) // Clothes layer .bind(req.l_clothes_0) .bind(req.l_clothes_1) .bind(req.l_clothes_2) .bind(req.l_clothes_3) .bind(req.l_clothes_4) .bind(req.l_clothes_5) .bind(req.l_clothes_6) .bind(req.l_clothes_7) .bind(req.l_clothes_8) // Accessories layer .bind(req.l_accessories_0) .bind(req.l_accessories_1) .bind(req.l_accessories_2) .bind(req.l_accessories_3) .bind(req.l_accessories_4) .bind(req.l_accessories_5) .bind(req.l_accessories_6) .bind(req.l_accessories_7) .bind(req.l_accessories_8) // Neutral emotion .bind(req.e_neutral_0) .bind(req.e_neutral_1) .bind(req.e_neutral_2) .bind(req.e_neutral_3) .bind(req.e_neutral_4) .bind(req.e_neutral_5) .bind(req.e_neutral_6) .bind(req.e_neutral_7) .bind(req.e_neutral_8) // Happy emotion .bind(req.e_happy_0) .bind(req.e_happy_1) .bind(req.e_happy_2) .bind(req.e_happy_3) .bind(req.e_happy_4) .bind(req.e_happy_5) .bind(req.e_happy_6) .bind(req.e_happy_7) .bind(req.e_happy_8) // Sad emotion .bind(req.e_sad_0) .bind(req.e_sad_1) .bind(req.e_sad_2) .bind(req.e_sad_3) .bind(req.e_sad_4) .bind(req.e_sad_5) .bind(req.e_sad_6) .bind(req.e_sad_7) .bind(req.e_sad_8) // Angry emotion .bind(req.e_angry_0) .bind(req.e_angry_1) .bind(req.e_angry_2) .bind(req.e_angry_3) .bind(req.e_angry_4) .bind(req.e_angry_5) .bind(req.e_angry_6) .bind(req.e_angry_7) .bind(req.e_angry_8) // Surprised emotion .bind(req.e_surprised_0) .bind(req.e_surprised_1) .bind(req.e_surprised_2) .bind(req.e_surprised_3) .bind(req.e_surprised_4) .bind(req.e_surprised_5) .bind(req.e_surprised_6) .bind(req.e_surprised_7) .bind(req.e_surprised_8) // Thinking emotion .bind(req.e_thinking_0) .bind(req.e_thinking_1) .bind(req.e_thinking_2) .bind(req.e_thinking_3) .bind(req.e_thinking_4) .bind(req.e_thinking_5) .bind(req.e_thinking_6) .bind(req.e_thinking_7) .bind(req.e_thinking_8) // Laughing emotion .bind(req.e_laughing_0) .bind(req.e_laughing_1) .bind(req.e_laughing_2) .bind(req.e_laughing_3) .bind(req.e_laughing_4) .bind(req.e_laughing_5) .bind(req.e_laughing_6) .bind(req.e_laughing_7) .bind(req.e_laughing_8) // Crying emotion .bind(req.e_crying_0) .bind(req.e_crying_1) .bind(req.e_crying_2) .bind(req.e_crying_3) .bind(req.e_crying_4) .bind(req.e_crying_5) .bind(req.e_crying_6) .bind(req.e_crying_7) .bind(req.e_crying_8) // Love emotion .bind(req.e_love_0) .bind(req.e_love_1) .bind(req.e_love_2) .bind(req.e_love_3) .bind(req.e_love_4) .bind(req.e_love_5) .bind(req.e_love_6) .bind(req.e_love_7) .bind(req.e_love_8) // Confused emotion .bind(req.e_confused_0) .bind(req.e_confused_1) .bind(req.e_confused_2) .bind(req.e_confused_3) .bind(req.e_confused_4) .bind(req.e_confused_5) .bind(req.e_confused_6) .bind(req.e_confused_7) .bind(req.e_confused_8) // Sleeping emotion .bind(req.e_sleeping_0) .bind(req.e_sleeping_1) .bind(req.e_sleeping_2) .bind(req.e_sleeping_3) .bind(req.e_sleeping_4) .bind(req.e_sleeping_5) .bind(req.e_sleeping_6) .bind(req.e_sleeping_7) .bind(req.e_sleeping_8) // Wink emotion .bind(req.e_wink_0) .bind(req.e_wink_1) .bind(req.e_wink_2) .bind(req.e_wink_3) .bind(req.e_wink_4) .bind(req.e_wink_5) .bind(req.e_wink_6) .bind(req.e_wink_7) .bind(req.e_wink_8) .fetch_one(executor) .await?; Ok(avatar) } /// Delete a realm avatar. pub async fn delete_realm_avatar<'e>( executor: impl PgExecutor<'e>, avatar_id: Uuid, ) -> Result<(), AppError> { sqlx::query( r#" DELETE FROM realm.avatars WHERE id = $1 "#, ) .bind(avatar_id) .execute(executor) .await?; Ok(()) }