chattyness/crates/chattyness-db/src/ws_messages.rs

237 lines
7 KiB
Rust

//! WebSocket message protocol for channel presence.
//!
//! Shared message types used by both server and WASM client.
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::models::{AvatarRenderData, ChannelMemberInfo, ChannelMemberWithAvatar, LooseProp};
/// Default function for serde that returns true (for is_same_scene field).
/// Must be pub for serde derive macro to access via full path.
pub fn default_is_same_scene() -> bool {
true
}
/// WebSocket configuration sent to client on connect.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WsConfig {
/// Interval for client to send ping to keep connection alive (seconds).
pub ping_interval_secs: u64,
}
/// WebSocket close codes (custom range: 4000-4999).
pub mod close_codes {
/// Scene change (user navigating to different scene).
pub const SCENE_CHANGE: u16 = 4000;
/// Server timeout (no message received within timeout period).
pub const SERVER_TIMEOUT: u16 = 4001;
}
/// Reason for member disconnect.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DisconnectReason {
/// Graceful disconnect (browser close, normal WebSocket close).
Graceful,
/// Scene navigation (custom close code 4000).
SceneChange,
/// Timeout (connection lost, no ping response).
Timeout,
}
/// Client-to-server WebSocket messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
/// Update position in the channel.
UpdatePosition {
/// X coordinate in scene space.
x: f64,
/// Y coordinate in scene space.
y: f64,
},
/// Update emotion by name.
UpdateEmotion {
/// Emotion name (e.g., "happy", "sad", "neutral").
emotion: String,
},
/// Ping to keep connection alive.
Ping,
/// Send a chat message to the channel or directly to a user.
SendChatMessage {
/// Message content (max 500 chars).
content: String,
/// Target display name for direct whisper. None = broadcast to scene.
#[serde(default)]
target_display_name: Option<String>,
},
/// Drop a prop from inventory to the canvas.
DropProp {
/// Inventory item ID to drop.
inventory_item_id: Uuid,
},
/// Pick up a loose prop from the canvas.
PickUpProp {
/// Loose prop ID to pick up.
loose_prop_id: Uuid,
},
/// Request to broadcast avatar appearance to other users.
SyncAvatar,
/// Request to teleport to a different scene.
Teleport {
/// Scene ID to teleport to.
scene_id: Uuid,
},
}
/// Server-to-client WebSocket messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage {
/// Welcome message with initial state after connection.
Welcome {
/// This user's member info.
member: ChannelMemberInfo,
/// All current members with avatars.
members: Vec<ChannelMemberWithAvatar>,
/// WebSocket configuration for the client.
config: WsConfig,
},
/// A member joined the channel.
MemberJoined {
/// The member that joined.
member: ChannelMemberWithAvatar,
},
/// A member left the channel.
MemberLeft {
/// User ID (if authenticated user).
user_id: Option<Uuid>,
/// Guest session ID (if guest).
guest_session_id: Option<Uuid>,
/// Reason for disconnect.
reason: DisconnectReason,
},
/// A member updated their position.
PositionUpdated {
/// User ID (if authenticated user).
user_id: Option<Uuid>,
/// Guest session ID (if guest).
guest_session_id: Option<Uuid>,
/// New X coordinate.
x: f64,
/// New Y coordinate.
y: f64,
},
/// A member changed their emotion.
EmotionUpdated {
/// User ID (if authenticated user).
user_id: Option<Uuid>,
/// Guest session ID (if guest).
guest_session_id: Option<Uuid>,
/// Emotion name (e.g., "happy", "sad", "neutral").
emotion: String,
/// Asset paths for all 9 positions of the new emotion layer.
emotion_layer: [Option<String>; 9],
},
/// Pong response to client ping.
Pong,
/// Error message.
Error {
/// Error code.
code: String,
/// Error message.
message: String,
},
/// A chat message was received.
ChatMessageReceived {
/// Unique message ID.
message_id: Uuid,
/// User ID of sender (if authenticated user).
user_id: Option<Uuid>,
/// Guest session ID (if guest).
guest_session_id: Option<Uuid>,
/// Display name of sender.
display_name: String,
/// Message content.
content: String,
/// Emotion name for bubble styling (e.g., "happy", "sad", "neutral").
emotion: String,
/// Sender's X position at time of message.
x: f64,
/// Sender's Y position at time of message.
y: f64,
/// Server timestamp (milliseconds since epoch).
timestamp: i64,
/// Whether this is a whisper (direct message).
/// Default: false (broadcast message).
#[serde(default)]
is_whisper: bool,
/// Whether sender is in the same scene as recipient.
/// For whispers: true = same scene (show as bubble), false = cross-scene (show as notification).
/// For broadcasts: always true.
/// Default: true (same scene).
#[serde(default = "self::default_is_same_scene")]
is_same_scene: bool,
},
/// Initial list of loose props when joining channel.
LoosePropsSync {
/// All current loose props in the channel.
props: Vec<LooseProp>,
},
/// A prop was dropped on the canvas.
PropDropped {
/// The dropped prop.
prop: LooseProp,
},
/// A prop was picked up from the canvas.
PropPickedUp {
/// ID of the prop that was picked up.
prop_id: Uuid,
/// User ID who picked it up (if authenticated).
picked_up_by_user_id: Option<Uuid>,
/// Guest session ID who picked it up (if guest).
picked_up_by_guest_id: Option<Uuid>,
},
/// A prop expired and was removed.
PropExpired {
/// 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,
},
/// Teleport approved - client should disconnect and reconnect to new scene.
TeleportApproved {
/// Scene ID to navigate to.
scene_id: Uuid,
/// Scene slug for URL.
scene_slug: String,
},
}