openws-server 0.1.1

WebSocket server with room-based messaging, private messages, HTTP broadcast, and configurable limits
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;

use tokio::sync::RwLock;
use uuid::Uuid;

use crate::config::AppConfig;
use crate::message::ServerMessage;
use crate::room::{ConnectedClient, Room};

#[derive(Clone)]
pub struct AppState {
    pub rooms: Arc<RwLock<HashMap<String, Room>>>,
    pub config: AppConfig,
    pub started_at: Instant,
}

impl AppState {
    pub fn new(config: AppConfig) -> Self {
        Self {
            rooms: Arc::new(RwLock::new(HashMap::new())),
            config,
            started_at: Instant::now(),
        }
    }

    pub async fn join_room(
        &self,
        room_name: &str,
        client: ConnectedClient,
    ) -> Result<(), String> {
        let mut rooms = self.rooms.write().await;

        if rooms.len() >= self.config.limits.max_rooms && !rooms.contains_key(room_name) {
            return Err("max rooms reached".into());
        }

        let room = rooms
            .entry(room_name.to_string())
            .or_insert_with(|| Room::new(room_name.to_string()));

        if room.clients.len() >= self.config.limits.max_clients {
            return Err("room is full".into());
        }

        room.clients.insert(client.id, client);
        Ok(())
    }

    pub async fn leave_room(&self, room_name: &str, client_id: Uuid) -> Option<String> {
        let mut rooms = self.rooms.write().await;
        let room = rooms.get_mut(room_name)?;
        let client = room.clients.remove(&client_id)?;
        let username = client.username.clone();
        if room.clients.is_empty() {
            rooms.remove(room_name);
        }
        Some(username)
    }

    pub async fn broadcast(&self, room_name: &str, msg: &ServerMessage, exclude_id: Option<Uuid>) {
        let mut rooms = self.rooms.write().await;
        if let Some(room) = rooms.get_mut(room_name) {
            room.broadcast(msg, exclude_id);
        }
    }

    pub async fn send_private(
        &self,
        from: &str,
        to_username: &str,
        text: &str,
    ) -> Result<(), String> {
        let rooms = self.rooms.read().await;
        for room in rooms.values() {
            if let Some(client) = room.find_client_by_username(to_username) {
                let msg = ServerMessage::Private {
                    from: from.into(),
                    text: text.into(),
                };
                let payload = serde_json::to_string(&msg).map_err(|e| e.to_string())?;
                client.sender.try_send(payload).map_err(|_| String::from("failed to send"))?;
                return Ok(());
            }
        }
        Err("user not found".into())
    }

    pub async fn stats(&self) -> serde_json::Value {
        let rooms = self.rooms.read().await;
        let total_clients: usize = rooms.values().map(|r| r.clients.len()).sum();
        let rooms_detail: Vec<serde_json::Value> = rooms
            .values()
            .map(|r| {
                serde_json::json!({
                    "name": r.name,
                    "clients": r.clients.len(),
                    "users": r.clients.values().map(|c| &c.username).collect::<Vec<_>>(),
                })
            })
            .collect();
        serde_json::json!({
            "rooms": rooms.len(),
            "clients": total_clients,
            "detail": rooms_detail,
        })
    }
}