Introduced the `delete_room` command to allow users to remove temporary chat rooms, with appropriate access checks for room creators and admins. Updated the `RoomMeta` structure to include the `created_by_chat_user_id` field for better tracking of room ownership. Enhanced error handling in room access validation for improved user feedback during room deletion and initialization processes.
1573 lines
50 KiB
Rust
1573 lines
50 KiB
Rust
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<RwLock<ChatState>>,
|
|
config: Arc<ServerConfig>,
|
|
) {
|
|
let trimmed = raw.trim();
|
|
if trimmed.is_empty() {
|
|
return;
|
|
}
|
|
match serde_json::from_str::<Command>(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<RwLock<ChatState>>,
|
|
config: Arc<ServerConfig>,
|
|
) {
|
|
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<RwLock<ChatState>>, 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<RwLock<ChatState>>,
|
|
config: Arc<ServerConfig>,
|
|
) {
|
|
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<RwLock<ChatState>>,
|
|
config: Arc<ServerConfig>,
|
|
) {
|
|
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<RwLock<ChatState>>,
|
|
) -> Option<Command> {
|
|
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<RwLock<ChatState>>) {
|
|
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<RwLock<ChatState>>) {
|
|
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<RwLock<ChatState>>) {
|
|
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<Value> = Vec::new();
|
|
let mut invalid_roll = false;
|
|
let mut already_rolled = false;
|
|
let mut round_snapshot: Option<(i32, HashMap<ClientId, i32>)> = None;
|
|
let mut total_snapshot: Option<HashMap<ClientId, i32>> = None;
|
|
let mut next_round_to_start: Option<i32> = 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<Value> = 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<Value> = 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<RwLock<ChatState>>,
|
|
config: Arc<ServerConfig>,
|
|
) {
|
|
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<RwLock<ChatState>>) {
|
|
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<RwLock<ChatState>>) {
|
|
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<RwLock<ChatState>>,
|
|
) {
|
|
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<RwLock<ChatState>>,
|
|
) {
|
|
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<RwLock<ChatState>>,
|
|
config: Arc<ServerConfig>,
|
|
) {
|
|
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<i32>,
|
|
min_age: Option<i32>,
|
|
max_age: Option<i32>,
|
|
password: Option<String>,
|
|
friends_of_owner_only: bool,
|
|
required_user_right_id: Option<i32>,
|
|
room_type_id: Option<i32>,
|
|
}
|
|
|
|
async fn handle_create_room_command(
|
|
client_id: ClientId,
|
|
command: &Command,
|
|
state: Arc<RwLock<ChatState>>,
|
|
_config: Arc<ServerConfig>,
|
|
) {
|
|
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<RwLock<ChatState>>,
|
|
config: Arc<ServerConfig>,
|
|
) {
|
|
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<i32>,
|
|
gender_id: Option<i32>,
|
|
age: Option<i32>,
|
|
right_type_ids: HashSet<i32>,
|
|
rights: HashSet<String>,
|
|
}
|
|
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>();
|
|
let mut rooms = guard
|
|
.room_meta
|
|
.iter()
|
|
.filter(|(name, _)| *name != &resolved_room)
|
|
.map(|(name, meta)| (name.clone(), meta.clone()))
|
|
.collect::<Vec<_>>();
|
|
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<String> = 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<RwLock<ChatState>>) {
|
|
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<i32>,
|
|
user_gender_id: Option<i32>,
|
|
age: Option<i32>,
|
|
user_right_type_ids: &HashSet<i32>,
|
|
user_rights: &HashSet<String>,
|
|
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<String> {
|
|
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<CreateRoomArgs, &'static str> {
|
|
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::<i32>().map_err(|_| "invalid_min_age")?);
|
|
}
|
|
"max_age" | "max" => {
|
|
parsed.max_age = Some(value.parse::<i32>().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::<i32>().map_err(|_| "invalid_required_right_id")?);
|
|
}
|
|
"type" | "type_id" | "room_type" | "room_type_id" => {
|
|
parsed.room_type_id = Some(value.parse::<i32>().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<bool> {
|
|
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<Option<i32>> {
|
|
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<String>) -> Option<Command> {
|
|
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<String> {
|
|
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<i32> {
|
|
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<i32> {
|
|
let trimmed = text.trim();
|
|
if trimmed.is_empty() {
|
|
return None;
|
|
}
|
|
if let Ok(value) = trimmed.parse::<i32>() {
|
|
return Some(value);
|
|
}
|
|
if let Some((_, rhs)) = trimmed.to_lowercase().split_once('d') {
|
|
return rhs.trim().parse::<i32>().ok();
|
|
}
|
|
None
|
|
}
|