use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
const MAX_HISTORY: usize = 1000;
const BROADCAST_CAPACITY: usize = 256;
#[derive(Clone, Debug)]
pub struct Message {
pub id: uuid::Uuid,
pub timestamp: DateTime<Utc>,
pub username: String,
pub content: MessageContent,
}
#[derive(Clone, Debug)]
pub enum MessageContent {
Text(String),
System(String),
}
impl Message {
pub fn text(username: impl Into<String>, content: impl Into<String>) -> Self {
Self {
id: uuid::Uuid::new_v4(),
timestamp: Utc::now(),
username: username.into(),
content: MessageContent::Text(content.into()),
}
}
pub fn system(content: impl Into<String>) -> Self {
Self {
id: uuid::Uuid::new_v4(),
timestamp: Utc::now(),
username: String::new(),
content: MessageContent::System(content.into()),
}
}
}
#[derive(Clone, Debug)]
pub enum RoomEvent {
NewMessage(Message),
UserJoined(String),
UserLeft(String),
Refresh,
}
#[derive(Clone, Debug)]
pub struct UserSession {
pub username: String,
pub joined_at: DateTime<Utc>,
pub terminal_size: (u16, u16), }
impl UserSession {
pub fn new(username: impl Into<String>, cols: u16, rows: u16) -> Self {
Self {
username: username.into(),
joined_at: Utc::now(),
terminal_size: (cols, rows),
}
}
}
pub struct ChatRoom {
pub name: String,
password_hash: String,
messages: RwLock<Vec<Message>>,
users: RwLock<HashMap<u64, UserSession>>,
broadcast_tx: broadcast::Sender<RoomEvent>,
max_connections: usize,
next_session_id: RwLock<u64>,
pub tunnel_url: RwLock<Option<String>>,
}
impl ChatRoom {
pub fn new(name: impl Into<String>, password_hash: String) -> Arc<Self> {
let (broadcast_tx, _) = broadcast::channel(BROADCAST_CAPACITY);
Arc::new(Self {
name: name.into(),
password_hash,
messages: RwLock::new(Vec::with_capacity(MAX_HISTORY)),
users: RwLock::new(HashMap::new()),
broadcast_tx,
max_connections: 50,
next_session_id: RwLock::new(1),
tunnel_url: RwLock::new(None),
})
}
pub fn verify_password(&self, password: &str) -> bool {
use argon2::{Argon2, PasswordHash, PasswordVerifier};
match PasswordHash::new(&self.password_hash) {
Ok(parsed_hash) => Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok(),
Err(_) => false,
}
}
pub fn hash_password(password: &str) -> anyhow::Result<String> {
use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHasher,
};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?;
Ok(hash.to_string())
}
pub async fn join(&self, username: String, cols: u16, rows: u16) -> anyhow::Result<u64> {
let users = self.users.read().await;
if users.len() >= self.max_connections {
anyhow::bail!("Room is full");
}
drop(users);
let mut session_id = self.next_session_id.write().await;
let id = *session_id;
*session_id += 1;
drop(session_id);
let session = UserSession::new(username.clone(), cols, rows);
self.users.write().await.insert(id, session);
let msg = Message::system(format!("{} joined the room", username));
self.add_message(msg).await;
let _ = self.broadcast_tx.send(RoomEvent::UserJoined(username));
Ok(id)
}
pub async fn leave(&self, session_id: u64) {
if let Some(session) = self.users.write().await.remove(&session_id) {
let msg = Message::system(format!("{} left the room", session.username));
self.add_message(msg).await;
let _ = self.broadcast_tx.send(RoomEvent::UserLeft(session.username));
}
}
pub async fn update_terminal_size(&self, session_id: u64, cols: u16, rows: u16) {
if let Some(session) = self.users.write().await.get_mut(&session_id) {
session.terminal_size = (cols, rows);
}
let _ = self.broadcast_tx.send(RoomEvent::Refresh);
}
pub async fn add_message(&self, message: Message) {
let mut messages = self.messages.write().await;
messages.push(message.clone());
let len = messages.len();
if len > MAX_HISTORY {
messages.drain(0..(len - MAX_HISTORY));
}
drop(messages);
let _ = self.broadcast_tx.send(RoomEvent::NewMessage(message));
}
pub async fn send_message(&self, session_id: u64, content: String) {
let users = self.users.read().await;
if let Some(session) = users.get(&session_id) {
let msg = Message::text(session.username.clone(), content);
drop(users);
self.add_message(msg).await;
}
}
pub async fn get_messages(&self) -> Vec<Message> {
self.messages.read().await.clone()
}
pub async fn get_users(&self) -> Vec<String> {
self.users
.read()
.await
.values()
.map(|s| s.username.clone())
.collect()
}
pub async fn user_count(&self) -> usize {
self.users.read().await.len()
}
pub fn subscribe(&self) -> broadcast::Receiver<RoomEvent> {
self.broadcast_tx.subscribe()
}
pub async fn set_tunnel_url(&self, url: String) {
*self.tunnel_url.write().await = Some(url);
let _ = self.broadcast_tx.send(RoomEvent::Refresh);
}
}