openws-server 0.1.2

WebSocket server with room-based messaging, private messages, HTTP broadcast, and configurable limits
mod broadcast;
mod config;
mod handler;
mod message;
mod room;
mod state;

use std::time::Instant;

use axum::routing::{get, post};
use axum::{Json, Router};
use tower_http::cors::CorsLayer;
use tracing::info;

use crate::config::mask_api_key;
use crate::state::AppState;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "openws_server=info".into()),
        )
        .init();

    let cfg = config::load_config().expect("failed to load configuration");
    let state = AppState::new(cfg.clone());
    let started_at: Instant = state.started_at;

    let app = Router::new()
        .route("/ws", get(handler::ws_handler))
        .route("/api/broadcast", post(broadcast::broadcast_handler))
        .route(
            "/health",
            {
                let state = state.clone();
                get(move || {
                    let state = state.clone();
                    async move {
                        let rooms = state.rooms.read().await;
                        let total_clients: usize = rooms.values().map(|r| r.clients.len()).sum();
                        Json(serde_json::json!({
                            "status": "ok",
                            "rooms": rooms.len(),
                            "clients": total_clients,
                            "uptime_secs": started_at.elapsed().as_secs(),
                        }))
                    }
                })
            },
        )
        .route(
            "/stats",
            {
                let state = state.clone();
                get(move || {
                    let state = state.clone();
                    async move { Json(state.stats().await) }
                })
            },
        )
        .layer(CorsLayer::permissive())
        .with_state(state);

    let bind_addr = format!("{}:{}", cfg.server.host, cfg.server.port);

    info!(
        "openws-server v{} starting on {}",
        env!("CARGO_PKG_VERSION"),
        bind_addr,
    );
    info!(
        "limits: max_clients={}, max_rooms={}, max_message_size={}, max_username_len={}",
        cfg.limits.max_clients,
        cfg.limits.max_rooms,
        cfg.limits.max_message_size,
        cfg.limits.max_username_len,
    );
    info!(
        "heartbeat: ping_interval={}s, pong_timeout={}s",
        cfg.heartbeat.ping_interval_secs, cfg.heartbeat.pong_timeout_secs,
    );
    info!(
        "broadcast api key: {}",
        mask_api_key(&cfg.broadcast.api_key)
    );

    let listener = tokio::net::TcpListener::bind(&bind_addr)
        .await
        .expect("failed to bind to address");
    axum::serve(listener, app)
        .await
        .expect("server error");
}