Initialisiere yourchat2 als eigenständigen Rust-Chatdienst und portiere die Kernfunktionen aus der Altanwendung.

Die Implementierung enthält modulare Command-/State-/DB-Strukturen, DB-basierte Authentifizierung inkl. Rechte- und Raumzugriffsprüfung sowie kompatible Chat- und Dice-Commands.

Made-with: Cursor
This commit is contained in:
Torsten Schulz (local)
2026-03-04 17:04:41 +01:00
commit 0b91b94ae1
10 changed files with 3453 additions and 0 deletions

154
src/state.rs Normal file
View File

@@ -0,0 +1,154 @@
use crate::types::{ChatState, ClientId, Command, RoomInfo};
use serde_json::{json, Value};
use std::sync::Arc;
use tokio::sync::RwLock;
pub async fn authorize(client_id: ClientId, command: &Command, state: Arc<RwLock<ChatState>>) -> bool {
let auth_error = {
let guard = state.read().await;
let Some(client) = guard.clients.get(&client_id) else {
return false;
};
if !client.logged_in {
Some("not_initialized")
} else {
match (&client.token, &command.token) {
(Some(expected), Some(got)) if expected == got => None,
(Some(_), Some(_)) => Some("invalid_token"),
(Some(_), None) => Some("missing_token"),
_ => None,
}
}
};
if let Some(message) = auth_error {
send_to_client(client_id, state, json!({"type":"error","message": message})).await;
false
} else {
true
}
}
pub async fn send_room_list(client_id: ClientId, state: Arc<RwLock<ChatState>>) {
let list = {
let guard = state.read().await;
let mut rooms: Vec<RoomInfo> = guard
.rooms
.iter()
.map(|(name, members)| RoomInfo {
name: name.clone(),
users: members.len(),
})
.collect();
rooms.sort_by(|a, b| a.name.cmp(&b.name));
rooms
};
send_to_client(client_id, state, json!({"type":3, "message": list})).await;
}
pub async fn send_user_list(client_id: ClientId, state: Arc<RwLock<ChatState>>) {
let users = {
let guard = state.read().await;
let Some(client) = guard.clients.get(&client_id) else {
return;
};
if client.room.is_empty() {
Vec::<Value>::new()
} else if let Some(members) = guard.rooms.get(&client.room) {
members
.iter()
.filter_map(|id| guard.clients.get(id))
.map(|u| json!({"name": u.user_name, "color": u.color}))
.collect::<Vec<_>>()
} else {
Vec::<Value>::new()
}
};
send_to_client(client_id, state, json!({"type":2, "message": users})).await;
}
pub async fn send_to_client(client_id: ClientId, state: Arc<RwLock<ChatState>>, payload: Value) {
let msg = payload.to_string();
let tx = {
let guard = state.read().await;
guard.clients.get(&client_id).map(|c| c.tx.clone())
};
if let Some(tx) = tx {
let _ = tx.send(msg);
}
}
pub async fn broadcast_all(state: Arc<RwLock<ChatState>>, payload: Value) {
let msg = payload.to_string();
let targets = {
let guard = state.read().await;
guard.clients.values().map(|c| c.tx.clone()).collect::<Vec<_>>()
};
for tx in targets {
let _ = tx.send(msg.clone());
}
}
pub async fn broadcast_room(
room: &str,
state: Arc<RwLock<ChatState>>,
payload: Value,
exclude: Option<ClientId>,
) {
let msg = payload.to_string();
let targets = {
let guard = state.read().await;
let Some(members) = guard.rooms.get(room) else {
return;
};
members
.iter()
.filter_map(|id| {
if exclude.is_some() && exclude == Some(*id) {
return None;
}
guard.clients.get(id).map(|c| c.tx.clone())
})
.collect::<Vec<_>>()
};
for tx in targets {
let _ = tx.send(msg.clone());
}
}
pub async fn disconnect_client(client_id: ClientId, state: Arc<RwLock<ChatState>>) {
let (old_room, old_name, token, was_logged_in) = {
let mut guard = state.write().await;
let Some(client) = guard.clients.remove(&client_id) else {
return;
};
if !client.room.is_empty() {
if let Some(members) = guard.rooms.get_mut(&client.room) {
members.remove(&client_id);
if members.is_empty() {
guard.rooms.remove(&client.room);
}
}
}
(client.room, client.user_name, client.token, client.logged_in)
};
if let Some(token) = token {
let mut guard = state.write().await;
guard.tokens.remove(&token);
if was_logged_in {
guard.logged_in_names.remove(&old_name);
}
}
if !old_room.is_empty() {
broadcast_room(
&old_room,
state,
json!({"type":"system","message": format!("{old_name} disconnected")}),
Some(client_id),
)
.await;
}
}