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

277 lines
8.2 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,
},
/// Moderator command (only processed if sender is a moderator).
ModCommand {
/// Subcommand name ("summon", "avatar", "teleport", "ban", etc.).
subcommand: String,
/// Arguments for the subcommand.
args: Vec<String>,
},
/// Request to refresh identity after registration (guest → user conversion).
/// Server will fetch updated user data and broadcast to all members.
RefreshIdentity,
}
/// 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,
},
/// User has been summoned by a moderator - triggers teleport.
Summoned {
/// Scene ID to teleport to.
scene_id: Uuid,
/// Scene slug for URL.
scene_slug: String,
/// Display name of the moderator who summoned.
summoned_by: String,
},
/// Result of a moderator command.
ModCommandResult {
/// Whether the command succeeded.
success: bool,
/// Human-readable result message.
message: String,
},
/// A member's identity was updated (e.g., guest registered as user).
MemberIdentityUpdated {
/// User ID of the member.
user_id: Uuid,
/// New display name.
display_name: String,
/// Whether the member is still a guest.
is_guest: bool,
},
}