use axum::{ extract::ws::{Message, WebSocket, WebSocketUpgrade}, response::{Html, IntoResponse}, routing::get, Router, }; use axum::extract::State; use rand::random_bool; use futures::stream::StreamExt; use serde_json; use serde_json::json; use serde::{Deserialize, Serialize}; use std::fs; use std::io::Write; use std::sync::mpsc; use std::sync::{Mutex, Arc}; use regex::Regex; #[derive(Deserialize,Serialize,Debug,Ord,Eq,PartialEq,PartialOrd,Clone)] struct Entry { score: u32, person: String, } #[derive(Clone)] struct AppState { tx: mpsc::Sender, hiscores: Arc>>, } static CHANCE: f64 = 1.0/3.0; #[tokio::main] async fn main() { let file_contents: String = fs::read_to_string("hiscores.json").unwrap(); let hiscores: Arc>> = Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap())); let hiscore_clone1 = Arc::clone(&hiscores); let (tx, rx) = mpsc::channel::(); tokio::spawn( async move { handle_hiscores(rx, hiscore_clone1); } ); let state = AppState { tx,hiscores: Arc::clone(&hiscores) }; let app = Router::new() .route("/", get(index)) .route("/ws", get(ws_handler)) .route("/leaderboard", get(leaderboard)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:8084") .await .unwrap(); println!("http://0.0.0.0:8084"); axum::serve(listener, app).await.unwrap(); } async fn index() -> Html<&'static str> { Html(include_str!("../index.html")) } async fn leaderboard() -> Html<&'static str> { Html(include_str!("../leaderboard.html")) } fn handle_hiscores(rx: mpsc::Receiver, hiscores_arc: Arc>>) { // Panic galore let mut hiscores = hiscores_arc.lock().unwrap(); hiscores.sort(); hiscores.reverse(); let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap(); drop(hiscores); let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap(); file.write_all(file_contents.as_bytes()).unwrap(); file.flush().unwrap(); loop { match rx.recv() { Ok(new_entry) => { let mut hiscores = hiscores_arc.lock().unwrap(); if new_entry.score > hiscores[19].score { println!("New leaderboard {new_entry:?}"); hiscores.push(new_entry); hiscores.sort(); hiscores.reverse(); hiscores.truncate(20); let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap(); drop(hiscores); let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap(); file.write_all(file_contents.as_bytes()).unwrap(); file.flush().unwrap(); } }, Err(error) => println!("{error}"), } } } async fn ws_handler( ws: WebSocketUpgrade, State(state): State, ) -> impl IntoResponse { ws.on_upgrade(move |socket| { let tx = state.tx.clone(); let hiscores = Arc::clone(&state.hiscores); async move { handle_socket(socket, tx, hiscores).await; } }) } async fn handle_socket(mut socket: WebSocket, tx: mpsc::Sender, hiscores_arc: Arc>>) { let mut value: u32 = 0; let msg = { let hiscores = hiscores_arc.lock().unwrap(); json!({ "hiscores": &*hiscores }).to_string() }; let name_message = socket.next().await.unwrap().unwrap(); let name: String = match name_message { Message::Text(text) => validate_name(text.to_string()), _ => "anon".to_string(), }; //println!("Client connected: {name}"); let _ = socket .send(Message::Text(msg.into())) .await; while let Some(msg) = socket.next().await { match msg { Ok(Message::Text(_)) => { if random_bool(CHANCE) { let _ = tx.send(Entry{ person: name.clone(), score: value }); value = 0 } // 1/3 chance of failing else { value += 1; } let _ = socket.send(Message::Text(value.to_string().into())).await; } Ok(Message::Close(_)) => { break; } _ => {} } } } fn validate_name(input: String) -> String { let input = input.trim(); if input == "null" { return "anon".to_string(); } // Length check if input.is_empty() || input.len() > 32 { return "anon".to_string(); } // Allow only letters, numbers, _ and - let re = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap(); if re.is_match(input) { input.to_string() } else { "anon".to_string() } }