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.
This commit is contained in:
238
src/commands.rs
238
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;
|
handle_create_room_command(client_id, ¤t, Arc::clone(&state), Arc::clone(&config)).await;
|
||||||
return;
|
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;
|
handle_unknown_command(client_id, ¤t, Arc::clone(&state)).await;
|
||||||
return;
|
return;
|
||||||
@@ -165,7 +169,7 @@ async fn handle_init_command(
|
|||||||
};
|
};
|
||||||
(resolved_room_name, room_meta)
|
(resolved_room_name, room_meta)
|
||||||
};
|
};
|
||||||
if !room_access_allowed(
|
if let Err(access_error) = room_access_allowed(
|
||||||
Some(&room_meta),
|
Some(&room_meta),
|
||||||
profile.falukant_user_id,
|
profile.falukant_user_id,
|
||||||
profile.gender_id,
|
profile.gender_id,
|
||||||
@@ -177,7 +181,7 @@ async fn handle_init_command(
|
|||||||
)
|
)
|
||||||
.await
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +319,7 @@ async fn handle_join_command(
|
|||||||
client.rights.clone(),
|
client.rights.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if !room_access_allowed(
|
if let Err(access_error) = room_access_allowed(
|
||||||
Some(&room_meta),
|
Some(&room_meta),
|
||||||
falukant_user_id,
|
falukant_user_id,
|
||||||
gender_id,
|
gender_id,
|
||||||
@@ -327,7 +331,7 @@ async fn handle_join_command(
|
|||||||
)
|
)
|
||||||
.await
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
let (from_room, user_name, actual_room_name) = {
|
let (from_room, user_name, actual_room_name) = {
|
||||||
@@ -871,6 +875,7 @@ async fn handle_create_room_command(
|
|||||||
max_age: parsed.max_age,
|
max_age: parsed.max_age,
|
||||||
is_public: parsed.is_public,
|
is_public: parsed.is_public,
|
||||||
owner_id: Some(owner_chat_user_id),
|
owner_id: Some(owner_chat_user_id),
|
||||||
|
created_by_chat_user_id: Some(owner_chat_user_id),
|
||||||
room_type_id: parsed.room_type_id,
|
room_type_id: parsed.room_type_id,
|
||||||
friends_of_owner_only: parsed.friends_of_owner_only,
|
friends_of_owner_only: parsed.friends_of_owner_only,
|
||||||
is_temporary: true,
|
is_temporary: true,
|
||||||
@@ -898,6 +903,183 @@ async fn handle_create_room_command(
|
|||||||
.await;
|
.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>>) {
|
async fn handle_unknown_command(client_id: ClientId, command: &Command, state: Arc<RwLock<ChatState>>) {
|
||||||
if command.password.is_some() {
|
if command.password.is_some() {
|
||||||
send_error(
|
send_error(
|
||||||
@@ -920,9 +1102,9 @@ async fn room_access_allowed(
|
|||||||
user_rights: &HashSet<String>,
|
user_rights: &HashSet<String>,
|
||||||
provided_password: &str,
|
provided_password: &str,
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
) -> bool {
|
) -> Result<(), &'static str> {
|
||||||
let Some(room) = room_meta else {
|
let Some(room) = room_meta else {
|
||||||
return false;
|
return Err("room_not_found");
|
||||||
};
|
};
|
||||||
let is_admin = user_rights.contains("admin");
|
let is_admin = user_rights.contains("admin");
|
||||||
let has_required_right = room
|
let has_required_right = room
|
||||||
@@ -933,73 +1115,73 @@ async fn room_access_allowed(
|
|||||||
|
|
||||||
if let Some(required_gender) = room.gender_restriction_id {
|
if let Some(required_gender) = room.gender_restriction_id {
|
||||||
if required_gender > 0 && user_gender_id != Some(required_gender) {
|
if required_gender > 0 && user_gender_id != Some(required_gender) {
|
||||||
return false;
|
return Err("room_gender_restricted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !room.is_public {
|
if !room.is_public {
|
||||||
let Some(owner_id) = room.owner_id else {
|
let Some(owner_id) = room.owner_id else {
|
||||||
return false;
|
return Err("room_private");
|
||||||
};
|
};
|
||||||
let Some(fid) = falukant_user_id else {
|
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;
|
let is_owner = db::user_is_room_owner(config, owner_id, fid).await;
|
||||||
if !is_owner && !is_admin && !has_required_right {
|
if !is_owner && !is_admin && !has_required_right {
|
||||||
return false;
|
return Err("room_private");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(room_password) = &room.password {
|
if let Some(room_password) = &room.password {
|
||||||
if !room_password.is_empty() && !password_matches(room_password, provided_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 let Some(min_age) = room.min_age {
|
||||||
if min_age > 0 {
|
if min_age > 0 {
|
||||||
let Some(fid) = falukant_user_id else {
|
let Some(fid) = falukant_user_id else {
|
||||||
return false;
|
return Err("room_age_data_missing");
|
||||||
};
|
};
|
||||||
if fid <= 0 {
|
if fid <= 0 {
|
||||||
return false;
|
return Err("room_age_data_missing");
|
||||||
}
|
}
|
||||||
let Some(user_age) = age else {
|
let Some(user_age) = age else {
|
||||||
return false;
|
return Err("room_age_data_missing");
|
||||||
};
|
};
|
||||||
if user_age < min_age {
|
if user_age < min_age {
|
||||||
return false;
|
return Err("room_min_age_not_met");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(max_age) = room.max_age {
|
if let Some(max_age) = room.max_age {
|
||||||
if max_age > 0 {
|
if max_age > 0 {
|
||||||
let Some(user_age) = age else {
|
let Some(user_age) = age else {
|
||||||
return false;
|
return Err("room_age_data_missing");
|
||||||
};
|
};
|
||||||
if user_age > max_age {
|
if user_age > max_age {
|
||||||
return false;
|
return Err("room_max_age_exceeded");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(required_right) = room.required_user_right_id {
|
if let Some(required_right) = room.required_user_right_id {
|
||||||
if required_right > 0 && !user_right_type_ids.contains(&required_right) {
|
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 {
|
if room.friends_of_owner_only {
|
||||||
let Some(owner_id) = room.owner_id else {
|
let Some(owner_id) = room.owner_id else {
|
||||||
return false;
|
return Err("room_friends_only");
|
||||||
};
|
};
|
||||||
let Some(fid) = falukant_user_id else {
|
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 {
|
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 {
|
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<String>) -> Option<Comman
|
|||||||
to: None,
|
to: None,
|
||||||
user_name: 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 {
|
"/do" => Some(Command {
|
||||||
cmd_type: Value::String("do".to_string()),
|
cmd_type: Value::String("do".to_string()),
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ pub async fn load_room_configs(config: &ServerConfig) -> Result<Vec<RoomMeta>, &
|
|||||||
max_age: row.get::<_, Option<i32>>("max_age"),
|
max_age: row.get::<_, Option<i32>>("max_age"),
|
||||||
is_public: row.get::<_, bool>("is_public"),
|
is_public: row.get::<_, bool>("is_public"),
|
||||||
owner_id: row.get::<_, Option<i32>>("owner_id"),
|
owner_id: row.get::<_, Option<i32>>("owner_id"),
|
||||||
|
created_by_chat_user_id: None,
|
||||||
room_type_id: row.get::<_, Option<i32>>("room_type_id"),
|
room_type_id: row.get::<_, Option<i32>>("room_type_id"),
|
||||||
friends_of_owner_only: row.get::<_, bool>("friends_of_owner_only"),
|
friends_of_owner_only: row.get::<_, bool>("friends_of_owner_only"),
|
||||||
is_temporary: false,
|
is_temporary: false,
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ pub(crate) struct RoomMeta {
|
|||||||
pub(crate) max_age: Option<i32>,
|
pub(crate) max_age: Option<i32>,
|
||||||
pub(crate) is_public: bool,
|
pub(crate) is_public: bool,
|
||||||
pub(crate) owner_id: Option<i32>,
|
pub(crate) owner_id: Option<i32>,
|
||||||
|
pub(crate) created_by_chat_user_id: Option<i32>,
|
||||||
pub(crate) room_type_id: Option<i32>,
|
pub(crate) room_type_id: Option<i32>,
|
||||||
pub(crate) friends_of_owner_only: bool,
|
pub(crate) friends_of_owner_only: bool,
|
||||||
pub(crate) is_temporary: bool,
|
pub(crate) is_temporary: bool,
|
||||||
|
|||||||
Reference in New Issue
Block a user