Add create room command and room management enhancements in yourchat2

Implemented the `create_room` command to allow users to create new chat rooms with customizable settings such as privacy, age restrictions, and ownership. Enhanced room management by introducing functions to mark rooms as occupied or possibly empty, and added cleanup logic for stale temporary rooms. Updated the `RoomMeta` structure to include new fields for room creation timestamps and temporary status, ensuring better room lifecycle management.
This commit is contained in:
Torsten Schulz (local)
2026-03-04 22:44:00 +01:00
parent 3eaf31d64f
commit d620b8f8ae
5 changed files with 342 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ 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;
@@ -100,6 +101,10 @@ pub async fn handle_command(
handle_reload_rooms_command(client_id, &current, Arc::clone(&state), Arc::clone(&config)).await;
return;
}
"create_room" => {
handle_create_room_command(client_id, &current, Arc::clone(&state), Arc::clone(&config)).await;
return;
}
_ => {
handle_unknown_command(client_id, &current, Arc::clone(&state)).await;
return;
@@ -176,7 +181,7 @@ async fn handle_init_command(
return;
}
let (token, user_name, actual_room_name) = {
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
@@ -247,8 +252,12 @@ async fn handle_init_command(
.entry(resolved_room_name.clone())
.or_default()
.insert(client_id);
(token, user_name, resolved_room_name)
(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,
@@ -343,6 +352,10 @@ async fn handle_join_command(
(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,
@@ -793,6 +806,98 @@ async fn handle_reload_rooms_command(
.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),
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_unknown_command(client_id: ClientId, command: &Command, state: Arc<RwLock<ChatState>>) {
if command.password.is_some() {
send_error(
@@ -960,6 +1065,120 @@ fn normalize_color(input: &str) -> String {
}
}
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('/') {
@@ -1061,6 +1280,18 @@ fn command_from_chat_line(message: &str, token: Option<String>) -> Option<Comman
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,
}),
"/do" => Some(Command {
cmd_type: Value::String("do".to_string()),
token,