From 553602d5b4c1e313a65defd8568fca8e863e032d Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 4 Mar 2026 23:08:22 +0100 Subject: [PATCH] Add delete room command and enhance room management in yourchat2 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. --- src/commands.rs | 238 +++++++++++++++++++++++++++++++++++++++++++----- src/db.rs | 1 + src/types.rs | 1 + 3 files changed, 218 insertions(+), 22 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 0a6b94a..8d18dd6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -105,6 +105,10 @@ pub async fn handle_command( 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; @@ -165,7 +169,7 @@ async fn handle_init_command( }; (resolved_room_name, room_meta) }; - if !room_access_allowed( + if let Err(access_error) = room_access_allowed( Some(&room_meta), profile.falukant_user_id, profile.gender_id, @@ -177,7 +181,7 @@ async fn handle_init_command( ) .await { - send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; + send_error(client_id, Arc::clone(&state), access_error).await; return; } @@ -315,7 +319,7 @@ async fn handle_join_command( client.rights.clone(), ) }; - if !room_access_allowed( + if let Err(access_error) = room_access_allowed( Some(&room_meta), falukant_user_id, gender_id, @@ -327,7 +331,7 @@ async fn handle_join_command( ) .await { - send_error(client_id, Arc::clone(&state), "room_not_found_or_join_failed").await; + send_error(client_id, Arc::clone(&state), access_error).await; return; } let (from_room, user_name, actual_room_name) = { @@ -871,6 +875,7 @@ async fn handle_create_room_command( 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, @@ -898,6 +903,183 @@ async fn handle_create_room_command( .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( @@ -920,9 +1102,9 @@ async fn room_access_allowed( user_rights: &HashSet, provided_password: &str, config: &ServerConfig, -) -> bool { +) -> Result<(), &'static str> { let Some(room) = room_meta else { - return false; + return Err("room_not_found"); }; let is_admin = user_rights.contains("admin"); let has_required_right = room @@ -933,73 +1115,73 @@ async fn room_access_allowed( if let Some(required_gender) = room.gender_restriction_id { if required_gender > 0 && user_gender_id != Some(required_gender) { - return false; + return Err("room_gender_restricted"); } } if !room.is_public { let Some(owner_id) = room.owner_id else { - return false; + return Err("room_private"); }; let Some(fid) = falukant_user_id else { - return false; + 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 false; + return Err("room_private"); } } if let Some(room_password) = &room.password { if !room_password.is_empty() && !password_matches(room_password, provided_password) { - return false; + 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 false; + return Err("room_age_data_missing"); }; if fid <= 0 { - return false; + return Err("room_age_data_missing"); } let Some(user_age) = age else { - return false; + return Err("room_age_data_missing"); }; if user_age < min_age { - return false; + 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 false; + return Err("room_age_data_missing"); }; if user_age > max_age { - return false; + 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 false; + return Err("room_required_right_missing"); } } if room.friends_of_owner_only { let Some(owner_id) = room.owner_id else { - return false; + return Err("room_friends_only"); }; let Some(fid) = falukant_user_id else { - return false; + return Err("room_friends_only"); }; if !db::is_friend_of_room_owner(config, owner_id, fid).await && !is_admin { - return false; + return Err("room_friends_only"); } } - true + Ok(()) } fn password_matches(stored_password_or_hash: &str, provided_password: &str) -> bool { @@ -1292,6 +1474,18 @@ fn command_from_chat_line(message: &str, token: Option) -> Option 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, diff --git a/src/db.rs b/src/db.rs index d775d83..656fe6b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -219,6 +219,7 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result, & max_age: row.get::<_, Option>("max_age"), is_public: row.get::<_, bool>("is_public"), owner_id: row.get::<_, Option>("owner_id"), + created_by_chat_user_id: None, room_type_id: row.get::<_, Option>("room_type_id"), friends_of_owner_only: row.get::<_, bool>("friends_of_owner_only"), is_temporary: false, diff --git a/src/types.rs b/src/types.rs index 13cd98d..1e78524 100644 --- a/src/types.rs +++ b/src/types.rs @@ -93,6 +93,7 @@ pub(crate) struct RoomMeta { pub(crate) max_age: Option, pub(crate) is_public: bool, pub(crate) owner_id: Option, + pub(crate) created_by_chat_user_id: Option, pub(crate) room_type_id: Option, pub(crate) friends_of_owner_only: bool, pub(crate) is_temporary: bool,