use crate::types::{ChatState, ClientId, Command, RoomMeta, ServerConfig}; use bcrypt::verify as bcrypt_verify; use serde_json::{json, Value}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::RwLock; use uuid::Uuid; use crate::db; use crate::state; pub async fn process_text_command( client_id: ClientId, raw: &str, state: Arc>, config: Arc, ) { let trimmed = raw.trim(); if trimmed.is_empty() { return; } match serde_json::from_str::(trimmed) { Ok(command) => { handle_command(client_id, command, state, config).await; } Err(err) => { state::send_to_client( client_id, state, json!({"type":"error","message": format!("invalid_json: {err}")}), ) .await; } } } pub async fn handle_command( client_id: ClientId, command: Command, state: Arc>, config: Arc, ) { let mut current = command; loop { match command_name(¤t).as_str() { "init" => { handle_init_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; return; } "join" => { handle_join_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; return; } "message" => { if let Some(next_command) = handle_message_command(client_id, ¤t, Arc::clone(&state)).await { current = next_command; continue; } return; } "scream" => { handle_scream_command(client_id, ¤t, Arc::clone(&state)).await; return; } "do" => { handle_do_command(client_id, ¤t, Arc::clone(&state)).await; return; } "dice" | "roll" => { handle_dice_command(client_id, ¤t, Arc::clone(&state)).await; return; } "color" => { handle_color_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; return; } "rooms" | "list_rooms" => { handle_rooms_command(client_id, ¤t, Arc::clone(&state)).await; return; } "userlist" => { handle_userlist_command(client_id, ¤t, Arc::clone(&state)).await; return; } "ping" => { state::send_to_client(client_id, Arc::clone(&state), json!({"type":"pong"})).await; return; } "start_dice_game" => { handle_start_dice_game_command(client_id, ¤t, Arc::clone(&state)).await; return; } "end_dice_game" => { handle_end_dice_game_command(client_id, ¤t, Arc::clone(&state)).await; return; } "reload_rooms" => { handle_reload_rooms_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; return; } "create_room" => { handle_create_room_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; return; } "delete_room" => { handle_delete_room_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await; return; } _ => { handle_unknown_command(client_id, ¤t, Arc::clone(&state)).await; return; } } } } fn command_name(command: &Command) -> String { match &command.cmd_type { Value::String(s) => s.to_lowercase(), Value::Number(n) => n.to_string(), _ => String::new(), } } async fn send_error(client_id: ClientId, state: Arc>, message: &str) { state::send_to_client(client_id, state, json!({"type":"error","message": message})).await; } async fn handle_init_command( client_id: ClientId, command: &Command, state: Arc>, config: Arc, ) { let Some(raw_name) = command.name.clone().or(command.user_name.clone()) else { send_error(client_id, Arc::clone(&state), "missing_name").await; return; }; let requested_user_name = raw_name.trim().to_string(); if !db::is_valid_username(&requested_user_name) { send_error(client_id, Arc::clone(&state), "invalid_username").await; return; } if !db::is_allowed_user(&requested_user_name, &config) { send_error(client_id, Arc::clone(&state), "user_not_allowed").await; return; } let profile = match db::load_user_profile(&requested_user_name, &config).await { Ok(profile) => profile, Err(error_message) => { send_error(client_id, Arc::clone(&state), error_message).await; return; } }; let room_name = normalize_room_name(command.room.as_deref().unwrap_or("lobby")); let room_password = command.password.clone().unwrap_or_default(); let (resolved_room_name, room_meta) = { let guard = state.read().await; let Some(resolved_room_name) = resolve_room_name(&guard, &room_name) else { send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; return; }; let Some(room_meta) = guard.room_meta.get(&resolved_room_name).cloned() else { send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; return; }; (resolved_room_name, room_meta) }; if let Err(access_error) = room_access_allowed( Some(&room_meta), profile.falukant_user_id, profile.gender_id, profile.age, &profile.right_type_ids, &profile.rights, &room_password, &config, ) .await { send_error(client_id, Arc::clone(&state), access_error).await; return; } let (token, user_name, actual_room_name, old_room_name) = { let mut guard = state.write().await; if guard.logged_in_names.contains(&requested_user_name) && guard .clients .iter() .any(|(id, c)| *id != client_id && c.logged_in && c.user_name == requested_user_name) { drop(guard); send_error(client_id, Arc::clone(&state), "loggedin").await; return; } if !guard.room_meta.contains_key(&resolved_room_name) { drop(guard); send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; return; } let (old_room, old_name, was_logged_in, user_name, token, new_token) = { let Some(client) = guard.clients.get_mut(&client_id) else { return; }; let old_room = client.room.clone(); let old_name = client.user_name.clone(); let was_logged_in = client.logged_in; client.user_name = profile.display_name.clone(); client.color = profile.color.clone(); client.falukant_user_id = profile.falukant_user_id; client.chat_user_id = profile.chat_user_id; client.gender_id = profile.gender_id; client.age = profile.age; client.rights = profile.rights.clone(); client.right_type_ids = profile.right_type_ids.clone(); client.logged_in = true; client.room = resolved_room_name.clone(); let mut new_token = None; if client.token.is_none() { let generated = Uuid::new_v4().to_string(); client.token = Some(generated.clone()); new_token = Some(generated); } ( old_room, old_name, was_logged_in, client.user_name.clone(), client.token.clone().unwrap_or_default(), new_token, ) }; if let Some(generated) = new_token { guard.tokens.insert(generated, client_id); } if was_logged_in { guard.logged_in_names.remove(&old_name); } guard.logged_in_names.insert(user_name.clone()); if !old_room.is_empty() { if let Some(members) = guard.rooms.get_mut(&old_room) { members.remove(&client_id); } } guard .rooms .entry(resolved_room_name.clone()) .or_default() .insert(client_id); (token, user_name, resolved_room_name, old_room) }; if !old_room_name.is_empty() { state::mark_room_possibly_empty(&old_room_name, Arc::clone(&state)).await; } state::mark_room_occupied(&actual_room_name, Arc::clone(&state)).await; state::send_to_client( client_id, Arc::clone(&state), json!({"type":1, "message": token}), ) .await; state::send_room_list(client_id, Arc::clone(&state)).await; state::send_to_client( client_id, Arc::clone(&state), json!({"type":5, "message":"room_entered", "to": actual_room_name}), ) .await; state::broadcast_room( &actual_room_name, Arc::clone(&state), json!({"type":"system","message": format!("{user_name} joined {actual_room_name}")}), Some(client_id), ) .await; } async fn handle_join_command( client_id: ClientId, command: &Command, state: Arc>, config: Arc, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let room = normalize_room_name(command.room.as_deref().or(command.name.as_deref()).unwrap_or("lobby")); let password = command.password.clone().unwrap_or_default(); let (resolved_room, room_meta, falukant_user_id, gender_id, age, right_type_ids, rights) = { let guard = state.read().await; let Some(resolved_room) = resolve_room_name(&guard, &room) else { send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; return; }; let Some(room_meta) = guard.room_meta.get(&resolved_room).cloned() else { send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; return; }; let Some(client) = guard.clients.get(&client_id) else { return; }; ( resolved_room, room_meta, client.falukant_user_id, client.gender_id, client.age, client.right_type_ids.clone(), client.rights.clone(), ) }; if let Err(access_error) = room_access_allowed( Some(&room_meta), falukant_user_id, gender_id, age, &right_type_ids, &rights, &password, &config, ) .await { send_error(client_id, Arc::clone(&state), access_error).await; return; } let (from_room, user_name, actual_room_name) = { let mut guard = state.write().await; if !guard.room_meta.contains_key(&resolved_room) { drop(guard); send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; return; } let Some(client) = guard.clients.get_mut(&client_id) else { return; }; let from_room = client.room.clone(); let user_name = client.user_name.clone(); client.room = resolved_room.clone(); if !from_room.is_empty() { if let Some(members) = guard.rooms.get_mut(&from_room) { members.remove(&client_id); } } guard.rooms.entry(resolved_room.clone()).or_default().insert(client_id); (from_room, user_name, resolved_room) }; if !from_room.is_empty() { state::mark_room_possibly_empty(&from_room, Arc::clone(&state)).await; } state::mark_room_occupied(&actual_room_name, Arc::clone(&state)).await; if !from_room.is_empty() && from_room != room { state::broadcast_room( &from_room, Arc::clone(&state), json!({"type":"system","message": format!("{user_name} left {from_room}")}), Some(client_id), ) .await; } state::send_to_client( client_id, Arc::clone(&state), json!({"type":5, "message":"room_entered", "to": actual_room_name}), ) .await; state::send_room_list(client_id, Arc::clone(&state)).await; } async fn handle_message_command( client_id: ClientId, command: &Command, state: Arc>, ) -> Option { if !state::authorize(client_id, command, Arc::clone(&state)).await { return None; } let text = command.message.clone().unwrap_or_default(); if text.trim().is_empty() { return None; } if let Some(cmd) = command_from_chat_line(&text, command.token.clone()) { return Some(cmd); } let (room, user_name, color) = { let guard = state.read().await; let Some(client) = guard.clients.get(&client_id) else { return None; }; (client.room.clone(), client.user_name.clone(), client.color.clone()) }; if room.is_empty() { return None; } state::broadcast_room( &room, Arc::clone(&state), json!({"type":"message", "userName": user_name, "message": text, "color": color}), None, ) .await; None } async fn handle_scream_command(client_id: ClientId, command: &Command, state: Arc>) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let text = command.message.clone().unwrap_or_default(); if text.trim().is_empty() { return; } let (user_name, color) = { let guard = state.read().await; let Some(client) = guard.clients.get(&client_id) else { return; }; (client.user_name.clone(), client.color.clone()) }; state::broadcast_all( Arc::clone(&state), json!({"type":6, "userName": user_name, "message": text, "color": color}), ) .await; } async fn handle_do_command(client_id: ClientId, command: &Command, state: Arc>) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let text = command .message .clone() .or_else(|| string_from_value(command.value.as_ref())) .unwrap_or_default(); if text.trim().is_empty() { return; } let target = command.to.clone(); let (room, user_name, color) = { let guard = state.read().await; let Some(client) = guard.clients.get(&client_id) else { return; }; ( client.room.clone(), client.user_name.clone(), client.color.clone().unwrap_or_else(|| "#000000".to_string()), ) }; if room.is_empty() { return; } if let Some(to_name) = target { state::broadcast_room( &room, Arc::clone(&state), json!({ "type": 7, "message": {"tr":"user_action","action": text,"to": to_name}, "userName": user_name, "color": color }), None, ) .await; } else { state::broadcast_room( &room, Arc::clone(&state), json!({"type":"system", "message": format!("* {} {}", user_name, text)}), None, ) .await; } } async fn handle_dice_command(client_id: ClientId, command: &Command, state: Arc>) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let expr = command .message .clone() .or_else(|| string_from_value(command.value.as_ref())) .unwrap_or_else(|| "1d6".to_string()); let numeric_roll = i32_from_command_value(command).or_else(|| parse_roll_value(&expr)); let (room, user_name, color) = { let guard = state.read().await; let Some(client) = guard.clients.get(&client_id) else { return; }; ( client.room.clone(), client.user_name.clone(), client.color.clone().unwrap_or_else(|| "#000000".to_string()), ) }; if room.is_empty() { return; } let mut game_events: Vec = Vec::new(); let mut invalid_roll = false; let mut already_rolled = false; let mut round_snapshot: Option<(i32, HashMap)> = None; let mut total_snapshot: Option> = None; let mut next_round_to_start: Option = None; let mut game_ended = false; { let mut guard = state.write().await; let member_count = guard.rooms.get(&room).map(|m| m.len()).unwrap_or(0); if let Some(game) = guard.dice_games.get_mut(&room) { if game.running { if let Some(rolled) = numeric_roll { if !(1..=6).contains(&rolled) { invalid_roll = true; } else if game.rolled_this_round.contains_key(&client_id) { already_rolled = true; } else { game.rolled_this_round.insert(client_id, rolled); *game.total_scores.entry(client_id).or_insert(0) += rolled; game_events.push(json!({ "type": 8, "message": {"tr":"dice_rolled","round": game.current_round,"value": rolled}, "userName": user_name, "color": color })); if member_count > 0 && game.rolled_this_round.len() >= member_count { round_snapshot = Some((game.current_round, game.rolled_this_round.clone())); if game.current_round >= game.total_rounds { total_snapshot = Some(game.total_scores.clone()); game.running = false; game_ended = true; } else { game.current_round += 1; game.rolled_this_round.clear(); next_round_to_start = Some(game.current_round); } } } } else { invalid_roll = true; } } } } if invalid_roll { send_error(client_id, Arc::clone(&state), "invalid_dice_value").await; return; } if already_rolled { send_error(client_id, Arc::clone(&state), "dice_already_done").await; return; } if let Some((round_number, rolled_values)) = round_snapshot { let guard = state.read().await; let mut round_results: Vec = Vec::new(); for (uid, value) in rolled_values { if let Some(c) = guard.clients.get(&uid) { round_results.push(json!({"userName": c.user_name, "value": value})); } } game_events.push(json!({ "type": 8, "message": {"tr":"dice_round_ended","round": round_number,"results": round_results} })); } if let Some(total_scores) = total_snapshot { let guard = state.read().await; let mut final_results: Vec = Vec::new(); for (uid, score) in total_scores { if let Some(c) = guard.clients.get(&uid) { final_results.push(json!({"userName": c.user_name, "score": score})); } } game_events.push(json!({ "type": 8, "message": {"tr":"dice_game_results","results": final_results} })); } if let Some(next_round) = next_round_to_start { game_events.push(json!({"type": 8, "message": {"tr":"dice_round_started","round": next_round}})); } if game_ended { game_events.push(json!({"type": 8, "message": {"tr":"dice_game_ended"}})); } if !game_events.is_empty() { for event in game_events { state::broadcast_room(&room, Arc::clone(&state), event, None).await; } } else { state::broadcast_room( &room, Arc::clone(&state), json!({"type":"system","message": format!("{user_name} rolled {expr}")}), None, ) .await; } } async fn handle_color_command( client_id: ClientId, command: &Command, state: Arc>, config: Arc, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let Some(mut color) = command .message .clone() .or_else(|| string_from_value(command.value.as_ref())) else { return; }; color = normalize_color(&color); if color.is_empty() { send_error(client_id, Arc::clone(&state), "invalid_color").await; return; } let (room, user_name, chat_user_id) = { let mut guard = state.write().await; let Some(client) = guard.clients.get_mut(&client_id) else { return; }; client.color = Some(color.clone()); (client.room.clone(), client.user_name.clone(), client.chat_user_id) }; if let Some(chat_user_id) = chat_user_id { let _ = db::save_user_color(&config, chat_user_id, &color).await; } if !room.is_empty() { state::broadcast_room( &room, Arc::clone(&state), json!({"type":5, "message":"color_changed", "userName": user_name, "color": color}), None, ) .await; } } async fn handle_rooms_command(client_id: ClientId, command: &Command, state: Arc>) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } state::send_room_list(client_id, Arc::clone(&state)).await; } async fn handle_userlist_command(client_id: ClientId, command: &Command, state: Arc>) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } state::send_user_list(client_id, Arc::clone(&state)).await; } async fn handle_start_dice_game_command( client_id: ClientId, command: &Command, state: Arc>, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let rounds = command .rounds .or_else(|| i32_from_command_value(command)) .or_else(|| command.message.as_deref().and_then(parse_roll_value)) .unwrap_or(0); if !(1..=10).contains(&rounds) { send_error(client_id, Arc::clone(&state), "invalid_rounds").await; return; } let (room, user_name, is_admin) = { let guard = state.read().await; let Some(client) = guard.clients.get(&client_id) else { return; }; ( client.room.clone(), client.user_name.clone(), client.rights.contains("admin"), ) }; if room.is_empty() { return; } if !is_admin { send_error(client_id, Arc::clone(&state), "permission_denied").await; return; } let started = { let mut guard = state.write().await; let game = guard.dice_games.entry(room.clone()).or_default(); if game.running { false } else { game.running = true; game.current_round = 1; game.total_rounds = rounds; game.rolled_this_round.clear(); game.total_scores.clear(); true } }; if !started { send_error(client_id, Arc::clone(&state), "dice_game_already_running").await; return; } state::broadcast_room( &room, Arc::clone(&state), json!({ "type":8, "message":{"tr":"dice_game_started","rounds":rounds,"round":1}, "userName": user_name }), None, ) .await; } async fn handle_end_dice_game_command( client_id: ClientId, command: &Command, state: Arc>, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let (room, is_admin) = { let guard = state.read().await; let Some(client) = guard.clients.get(&client_id) else { return; }; (client.room.clone(), client.rights.contains("admin")) }; if room.is_empty() { return; } if !is_admin { send_error(client_id, Arc::clone(&state), "permission_denied").await; return; } let ended = { let mut guard = state.write().await; if let Some(game) = guard.dice_games.get_mut(&room) { if game.running { game.running = false; true } else { false } } else { false } }; if ended { state::broadcast_room( &room, Arc::clone(&state), json!({"type":8,"message":{"tr":"dice_game_ended"}}), None, ) .await; } else { send_error(client_id, Arc::clone(&state), "dice_game_not_running").await; } } async fn handle_reload_rooms_command( client_id: ClientId, command: &Command, state: Arc>, config: Arc, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } if let Ok(rooms) = db::load_room_configs(&config).await { let mut guard = state.write().await; for room in rooms { guard.rooms.entry(room.name.clone()).or_default(); guard.room_meta.insert(room.name.clone(), room); } } state::send_room_list(client_id, Arc::clone(&state)).await; state::send_to_client( client_id, Arc::clone(&state), json!({"type":"system","message":"rooms_reloaded"}), ) .await; } #[derive(Debug, Clone)] struct CreateRoomArgs { room_name: String, is_public: bool, gender_restriction_id: Option, min_age: Option, max_age: Option, password: Option, friends_of_owner_only: bool, required_user_right_id: Option, room_type_id: Option, } async fn handle_create_room_command( client_id: ClientId, command: &Command, state: Arc>, _config: Arc, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let raw = command .message .clone() .or_else(|| string_from_value(command.value.as_ref())) .unwrap_or_default(); let parsed = match parse_create_room_args(&raw) { Ok(v) => v, Err(msg) => { send_error(client_id, Arc::clone(&state), msg).await; return; } }; let owner_chat_user_id = { let guard = state.read().await; let Some(client) = guard.clients.get(&client_id) else { return; }; let Some(chat_user_id) = client.chat_user_id else { send_error(client_id, Arc::clone(&state), "missing_chat_user_for_room_creation").await; return; }; chat_user_id }; { let mut guard = state.write().await; if resolve_room_name(&guard, &parsed.room_name).is_some() { drop(guard); send_error(client_id, Arc::clone(&state), "room_already_exists").await; return; } guard.rooms.entry(parsed.room_name.clone()).or_default(); guard.room_meta.insert( parsed.room_name.clone(), RoomMeta { name: parsed.room_name.clone(), password: parsed.password.clone(), gender_restriction_id: parsed.gender_restriction_id, required_user_right_id: parsed.required_user_right_id, min_age: parsed.min_age, max_age: parsed.max_age, is_public: parsed.is_public, owner_id: Some(owner_chat_user_id), created_by_chat_user_id: Some(owner_chat_user_id), room_type_id: parsed.room_type_id, friends_of_owner_only: parsed.friends_of_owner_only, is_temporary: true, created_at_unix: Some(now_unix()), empty_since_unix: Some(now_unix()), }, ); } state::send_room_list(client_id, Arc::clone(&state)).await; state::send_to_client( client_id, Arc::clone(&state), json!({ "type":"system", "message":"room_created", "room": parsed.room_name, "is_public": parsed.is_public, "friends_of_owner_only": parsed.friends_of_owner_only, "gender": parsed.gender_restriction_id.map(|v| if v == 1 { "m" } else { "f" }), "min_age": parsed.min_age, "max_age": parsed.max_age, "required_user_right_id": parsed.required_user_right_id }), ) .await; } async fn handle_delete_room_command( client_id: ClientId, command: &Command, state: Arc>, config: Arc, ) { if !state::authorize(client_id, command, Arc::clone(&state)).await { return; } let room_name = command .room .clone() .or(command.name.clone()) .or(command.message.clone()) .unwrap_or_default(); let room_name = room_name.trim().to_string(); if room_name.is_empty() { send_error(client_id, Arc::clone(&state), "missing_room_name").await; return; } #[derive(Clone)] struct UserSnapshot { client_id: ClientId, user_name: String, falukant_user_id: Option, gender_id: Option, age: Option, right_type_ids: HashSet, rights: HashSet, } let (resolved_room, room_meta, requester_chat_user_id, requester_is_admin, room_members, users, candidate_rooms) = { let guard = state.read().await; let Some(resolved_room) = resolve_room_name(&guard, &room_name) else { send_error(client_id, Arc::clone(&state), "room_not_found").await; return; }; let Some(room_meta) = guard.room_meta.get(&resolved_room).cloned() else { send_error(client_id, Arc::clone(&state), "room_not_found").await; return; }; if !room_meta.is_temporary { send_error(client_id, Arc::clone(&state), "only_temporary_rooms_can_be_deleted").await; return; } let Some(requester) = guard.clients.get(&client_id) else { return; }; let requester_chat_user_id = requester.chat_user_id; let requester_is_admin = requester.rights.contains("admin"); let members = guard .rooms .get(&resolved_room) .cloned() .unwrap_or_default() .into_iter() .collect::>(); let users = members .iter() .filter_map(|id| { let c = guard.clients.get(id)?; Some(UserSnapshot { client_id: *id, user_name: c.user_name.clone(), falukant_user_id: c.falukant_user_id, gender_id: c.gender_id, age: c.age, right_type_ids: c.right_type_ids.clone(), rights: c.rights.clone(), }) }) .collect::>(); let mut rooms = guard .room_meta .iter() .filter(|(name, _)| *name != &resolved_room) .map(|(name, meta)| (name.clone(), meta.clone())) .collect::>(); rooms.sort_by(|a, b| a.0.cmp(&b.0)); ( resolved_room, room_meta, requester_chat_user_id, requester_is_admin, members, users, rooms, ) }; let creator_can_delete = requester_chat_user_id.is_some() && room_meta.created_by_chat_user_id.is_some() && requester_chat_user_id == room_meta.created_by_chat_user_id; if !creator_can_delete && !requester_is_admin { send_error(client_id, Arc::clone(&state), "permission_denied").await; return; } let mut reassignments: Vec<(ClientId, String, String)> = Vec::new(); for user in &users { let mut assigned: Option = None; for (candidate_name, candidate_meta) in &candidate_rooms { if room_access_allowed( Some(candidate_meta), user.falukant_user_id, user.gender_id, user.age, &user.right_type_ids, &user.rights, "", &config, ) .await .is_ok() { assigned = Some(candidate_name.clone()); break; } } let Some(target_room) = assigned else { send_error(client_id, Arc::clone(&state), "no_fallback_room_for_all_users").await; return; }; reassignments.push((user.client_id, user.user_name.clone(), target_room)); } { let mut guard = state.write().await; if !guard.room_meta.contains_key(&resolved_room) { drop(guard); send_error(client_id, Arc::clone(&state), "room_not_found").await; return; } guard.room_meta.remove(&resolved_room); guard.rooms.remove(&resolved_room); guard.dice_games.remove(&resolved_room); for (uid, _uname, target_room) in &reassignments { if let Some(client) = guard.clients.get_mut(uid) { client.room = target_room.clone(); } guard.rooms.entry(target_room.clone()).or_default().insert(*uid); } } for (_uid, _uname, target_room) in &reassignments { state::mark_room_occupied(target_room, Arc::clone(&state)).await; } for (uid, uname, target_room) in &reassignments { state::send_to_client( *uid, Arc::clone(&state), json!({"type":5, "message":"room_entered", "to": target_room}), ) .await; state::broadcast_room( target_room, Arc::clone(&state), json!({"type":"system","message": format!("{uname} joined {target_room}")}), Some(*uid), ) .await; } for uid in room_members { state::send_room_list(uid, Arc::clone(&state)).await; } state::send_to_client( client_id, Arc::clone(&state), json!({"type":"system","message":"room_deleted","room": resolved_room}), ) .await; } async fn handle_unknown_command(client_id: ClientId, command: &Command, state: Arc>) { if command.password.is_some() { send_error( client_id, Arc::clone(&state), "password_protected_rooms_not_supported_yet", ) .await; } else { send_error(client_id, Arc::clone(&state), "unknown_command").await; } } async fn room_access_allowed( room_meta: Option<&RoomMeta>, falukant_user_id: Option, user_gender_id: Option, age: Option, user_right_type_ids: &HashSet, user_rights: &HashSet, provided_password: &str, config: &ServerConfig, ) -> Result<(), &'static str> { let Some(room) = room_meta else { return Err("room_not_found"); }; let is_admin = user_rights.contains("admin"); let has_required_right = room .required_user_right_id .filter(|v| *v > 0) .map(|v| user_right_type_ids.contains(&v)) .unwrap_or(false); if let Some(required_gender) = room.gender_restriction_id { if required_gender > 0 && user_gender_id != Some(required_gender) { return Err("room_gender_restricted"); } } if !room.is_public { let Some(owner_id) = room.owner_id else { return Err("room_private"); }; let Some(fid) = falukant_user_id else { return Err("room_private"); }; let is_owner = db::user_is_room_owner(config, owner_id, fid).await; if !is_owner && !is_admin && !has_required_right { return Err("room_private"); } } if let Some(room_password) = &room.password { if !room_password.is_empty() && !password_matches(room_password, provided_password) { return Err("room_password_invalid"); } } if let Some(min_age) = room.min_age { if min_age > 0 { let Some(fid) = falukant_user_id else { return Err("room_age_data_missing"); }; if fid <= 0 { return Err("room_age_data_missing"); } let Some(user_age) = age else { return Err("room_age_data_missing"); }; if user_age < min_age { return Err("room_min_age_not_met"); } } } if let Some(max_age) = room.max_age { if max_age > 0 { let Some(user_age) = age else { return Err("room_age_data_missing"); }; if user_age > max_age { return Err("room_max_age_exceeded"); } } } if let Some(required_right) = room.required_user_right_id { if required_right > 0 && !user_right_type_ids.contains(&required_right) { return Err("room_required_right_missing"); } } if room.friends_of_owner_only { let Some(owner_id) = room.owner_id else { return Err("room_friends_only"); }; let Some(fid) = falukant_user_id else { return Err("room_friends_only"); }; if !db::is_friend_of_room_owner(config, owner_id, fid).await && !is_admin { return Err("room_friends_only"); } } Ok(()) } fn password_matches(stored_password_or_hash: &str, provided_password: &str) -> bool { if stored_password_or_hash.is_empty() { return true; } if stored_password_or_hash == provided_password { return true; } if provided_password.is_empty() { return false; } if stored_password_or_hash.starts_with("$2a$") || stored_password_or_hash.starts_with("$2b$") || stored_password_or_hash.starts_with("$2x$") || stored_password_or_hash.starts_with("$2y$") { return bcrypt_verify(provided_password, stored_password_or_hash).unwrap_or(false); } false } fn resolve_room_name(state: &ChatState, requested: &str) -> Option { if state.room_meta.contains_key(requested) { return Some(requested.to_string()); } let requested_lower = requested.to_lowercase(); state .room_meta .keys() .find(|name| name.to_lowercase() == requested_lower) .cloned() } fn normalize_room_name(input: &str) -> String { let trimmed = input.trim(); if trimmed.is_empty() { return "lobby".to_string(); } trimmed.to_string() } fn normalize_color(input: &str) -> String { let mut value = input.trim().to_string(); if value.is_empty() { return String::new(); } if !value.starts_with('#') { value = format!("#{value}"); } let raw = value.trim_start_matches('#'); match raw.len() { 3 if raw.chars().all(|c| c.is_ascii_hexdigit()) => { let mut expanded = String::from("#"); for ch in raw.chars() { expanded.push(ch); expanded.push(ch); } expanded } 6 if raw.chars().all(|c| c.is_ascii_hexdigit()) => format!("#{raw}"), _ => String::new(), } } fn parse_create_room_args(raw: &str) -> Result { let trimmed = raw.trim(); if trimmed.is_empty() { return Err("missing_room_name"); } let mut parts = trimmed.split_whitespace(); let room_name = parts.next().unwrap_or_default().trim().to_string(); if room_name.is_empty() { return Err("missing_room_name"); } if room_name.len() > 64 { return Err("room_name_too_long"); } let mut parsed = CreateRoomArgs { room_name, is_public: true, gender_restriction_id: None, min_age: None, max_age: None, password: None, friends_of_owner_only: false, required_user_right_id: None, room_type_id: None, }; for token in parts { let lower = token.to_lowercase(); if lower == "public" { parsed.is_public = true; continue; } if lower == "private" { parsed.is_public = false; continue; } if lower == "friends" || lower == "friends_only" { parsed.friends_of_owner_only = true; continue; } if lower == "nofriends" || lower == "no_friends" { parsed.friends_of_owner_only = false; continue; } let Some((key, value_raw)) = token.split_once('=') else { return Err("invalid_create_room_param"); }; let key = key.trim().to_lowercase(); let value = value_raw.trim(); if value.is_empty() { continue; } match key.as_str() { "public" | "is_public" => { let b = parse_bool(value).ok_or("invalid_boolean_param")?; parsed.is_public = b; } "friends_only" | "friends_of_owner_only" | "friends" => { let b = parse_bool(value).ok_or("invalid_boolean_param")?; parsed.friends_of_owner_only = b; } "gender" | "g" => { parsed.gender_restriction_id = parse_gender_mf(value).ok_or("invalid_gender_param_use_m_or_f")?; } "min_age" | "min" => { parsed.min_age = Some(value.parse::().map_err(|_| "invalid_min_age")?); } "max_age" | "max" => { parsed.max_age = Some(value.parse::().map_err(|_| "invalid_max_age")?); } "password" | "pw" => { parsed.password = Some(value.to_string()); } "right" | "right_id" | "required_right" | "required_user_right_id" => { parsed.required_user_right_id = Some(value.parse::().map_err(|_| "invalid_required_right_id")?); } "type" | "type_id" | "room_type" | "room_type_id" => { parsed.room_type_id = Some(value.parse::().map_err(|_| "invalid_room_type_id")?); } _ => return Err("unknown_create_room_param"), } } if let (Some(min_age), Some(max_age)) = (parsed.min_age, parsed.max_age) { if min_age > max_age { return Err("invalid_age_range"); } } Ok(parsed) } fn parse_bool(value: &str) -> Option { match value.trim().to_lowercase().as_str() { "1" | "true" | "yes" | "y" | "on" => Some(true), "0" | "false" | "no" | "n" | "off" => Some(false), _ => None, } } fn parse_gender_mf(value: &str) -> Option> { match value.trim().to_lowercase().as_str() { "m" | "male" => Some(Some(1)), "f" | "female" => Some(Some(2)), "any" | "none" | "-" => Some(None), _ => None, } } fn now_unix() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0) } fn command_from_chat_line(message: &str, token: Option) -> Option { let trimmed = message.trim(); if !trimmed.starts_with('/') { return None; } let mut parts = trimmed.splitn(2, ' '); let command = parts.next().unwrap_or_default().to_lowercase(); let rest = parts.next().unwrap_or("").trim().to_string(); match command.as_str() { "/join" => { if rest.is_empty() { return None; } let mut args = rest.splitn(2, ' '); let room = args.next().unwrap_or_default().trim().to_string(); let password = args.next().map(|p| p.trim().to_string()); Some(Command { cmd_type: Value::String("join".to_string()), token, name: None, room: Some(room), message: None, value: None, password, rounds: None, to: None, user_name: None, }) } "/color" => Some(Command { cmd_type: Value::String("color".to_string()), token, name: None, room: None, message: Some(rest), value: None, password: None, rounds: None, to: None, user_name: None, }), "/dice" => Some(Command { cmd_type: Value::String("dice".to_string()), token, name: None, room: None, message: Some(if rest.is_empty() { "1d6".to_string() } else { rest }), value: None, password: None, rounds: None, to: None, user_name: None, }), "/roll" => Some(Command { cmd_type: Value::String("roll".to_string()), token, name: None, room: None, message: Some(rest), value: None, password: None, rounds: None, to: None, user_name: None, }), "/start_dice_game" => Some(Command { cmd_type: Value::String("start_dice_game".to_string()), token, name: None, room: None, message: Some(rest.clone()), value: if rest.is_empty() { None } else { Some(Value::String(rest)) }, password: None, rounds: None, to: None, user_name: None, }), "/end_dice_game" => Some(Command { cmd_type: Value::String("end_dice_game".to_string()), token, name: None, room: None, message: None, value: None, password: None, rounds: None, to: None, user_name: None, }), "/reload_rooms" => Some(Command { cmd_type: Value::String("reload_rooms".to_string()), token, name: None, room: None, message: None, value: None, password: None, rounds: None, to: None, user_name: None, }), "/cr" | "/create_room" => Some(Command { cmd_type: Value::String("create_room".to_string()), token, name: None, room: None, message: Some(rest), value: None, password: None, rounds: None, to: None, user_name: None, }), "/dr" | "/delete_room" => Some(Command { cmd_type: Value::String("delete_room".to_string()), token, name: None, room: None, message: Some(rest), value: None, password: None, rounds: None, to: None, user_name: None, }), "/do" => Some(Command { cmd_type: Value::String("do".to_string()), token, name: None, room: None, message: Some(rest), value: None, password: None, rounds: None, to: None, user_name: None, }), "/scream" => Some(Command { cmd_type: Value::String("scream".to_string()), token, name: None, room: None, message: Some(rest), value: None, password: None, rounds: None, to: None, user_name: None, }), "/rooms" | "/list_rooms" => Some(Command { cmd_type: Value::String("rooms".to_string()), token, name: None, room: None, message: None, value: None, password: None, rounds: None, to: None, user_name: None, }), "/userlist" => Some(Command { cmd_type: Value::String("userlist".to_string()), token, name: None, room: None, message: None, value: None, password: None, rounds: None, to: None, user_name: None, }), _ => None, } } fn string_from_value(value: Option<&Value>) -> Option { let value = value?; match value { Value::String(s) => Some(s.clone()), Value::Number(n) => Some(n.to_string()), Value::Bool(b) => Some(b.to_string()), _ => None, } } fn i32_from_command_value(command: &Command) -> Option { let value = command.value.as_ref()?; match value { Value::Number(n) => n.as_i64().and_then(|v| i32::try_from(v).ok()), Value::String(s) => parse_roll_value(s), _ => None, } } fn parse_roll_value(text: &str) -> Option { let trimmed = text.trim(); if trimmed.is_empty() { return None; } if let Ok(value) = trimmed.parse::() { return Some(value); } if let Some((_, rhs)) = trimmed.to_lowercase().split_once('d') { return rhs.trim().parse::().ok(); } None }