summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJomar Milan <jomarm@jomarm.com>2026-05-31 12:22:41 -0700
committerJomar Milan <jomarm@jomarm.com>2026-05-31 12:22:41 -0700
commita2e55df0eeea9709175dd0a26c1e09bdaa60841a (patch)
tree584103be1f6b7d615cbc8d4e117e0107732fedda
parente104c8ed4a41e790dfd03e822a923c4df7b082b3 (diff)
Add websocket route
-rw-r--r--Cargo.lock172
-rw-r--r--Cargo.toml2
-rw-r--r--assets/session.js7
-rw-r--r--src/main.rs44
-rw-r--r--src/session.rs6
-rw-r--r--src/template.rs1
-rw-r--r--templates/session.html4
7 files changed, 220 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2da6bb1..6c7cba4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -68,6 +68,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
+ "base64",
"bytes",
"form_urlencoded",
"futures-util",
@@ -86,8 +87,10 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
+ "sha1",
"sync_wrapper",
"tokio",
+ "tokio-tungstenite",
"tower",
"tower-layer",
"tower-service",
@@ -114,6 +117,12 @@ dependencies = [
]
[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
name = "basic-toml"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -169,6 +178,12 @@ dependencies = [
]
[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -213,6 +228,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -225,6 +246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
+ "futures-sink",
"futures-task",
"pin-project-lite",
"slab",
@@ -241,6 +263,18 @@ dependencies = [
]
[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -424,6 +458,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -442,6 +485,41 @@ dependencies = [
]
[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -589,6 +667,17 @@ dependencies = [
]
[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -649,6 +738,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "tokio"
version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -677,6 +786,18 @@ dependencies = [
]
[[package]]
+name = "tokio-tungstenite"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -725,6 +846,22 @@ dependencies = [
]
[[package]]
+name = "tungstenite"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand",
+ "sha1",
+ "thiserror",
+]
+
+[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -759,6 +896,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -792,6 +938,32 @@ dependencies = [
]
[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 593b989..03a3e0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2024"
[dependencies]
askama = "0.16.0"
-axum = "0.8.9"
+axum = { version = "0.8.9", features = ["ws"] }
rust-embed = "8.11.0"
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.52.3", features = ["full"] }
diff --git a/assets/session.js b/assets/session.js
index b4f95dd..7d00838 100644
--- a/assets/session.js
+++ b/assets/session.js
@@ -1,5 +1,12 @@
const colorSelect = document.getElementById('color-select');
+const id = document.getElementById('session-script').dataset.id;
+const websocket = new WebSocket(`/session/${id}/play`);
+
+websocket.addEventListener('open', () => {
+ colorSelect.disabled = false;
+});
+
colorSelect.addEventListener('change', () => {
colorSelect.disabled = true;
}); \ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index ec09da2..32e5d58 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,13 +4,13 @@ mod template;
use crate::session::{HandObject, Session};
use crate::template::{IndexTemplate, SessionTemplate};
use askama::Template;
-use axum::extract::{Path, Query, State};
-use axum::http::{header, StatusCode};
+use axum::extract::ws::WebSocket;
+use axum::extract::{Path, Query, State, WebSocketUpgrade};
+use axum::http::{StatusCode, header};
use axum::response::{Html, IntoResponse, Response};
-use axum::routing::{get, put};
+use axum::routing::{any, get};
use axum::{Json, Router};
use rust_embed::Embed;
-use serde::Deserialize;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
@@ -18,7 +18,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Embed)]
#[folder = "assets/"]
-struct Asset;
+struct EmbedAsset;
struct AppState {
sessions: Mutex<HashMap<String, Session>>,
@@ -38,7 +38,8 @@ async fn main() {
.route("/", get(serve_index))
.route("/dist/{*path}", get(serve_static))
.route("/session/{id}", get(visit_session).put(create_session))
- .route("/session/{id}/hands", put(update_hands))
+ .route("/session/{id}/hands", get(serve_hands).put(update_hands))
+ .route("/session/{id}/play", any(upgrade_play))
.with_state(Arc::new(AppState::new()));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
@@ -64,9 +65,9 @@ async fn serve_index(State(state): State<Arc<AppState>>) -> Response {
}
async fn serve_static(Path(path): Path<String>) -> Response {
- match Asset::get(path.as_str()) {
+ match EmbedAsset::get(path.as_str()) {
Some(content) => {
- let mime = match path.split('.').last() {
+ let mime = match path.split('.').next_back() {
Some("js") => "application/javascript",
_ => "application/octet-stream",
};
@@ -88,7 +89,7 @@ async fn visit_session(
match sessions.get(&id) {
Some(session) => match passcode {
Some(passcode) if passcode.as_str() == session.passcode => {
- serve_template(SessionTemplate { session: &session })
+ serve_template(SessionTemplate { id: &id, session })
}
_ => (StatusCode::FORBIDDEN, "Incorrect session passcode").into_response(),
},
@@ -101,10 +102,7 @@ async fn create_session(
Query(query): Query<HashMap<String, String>>,
State(state): State<Arc<AppState>>,
) -> Response {
- let name = query
- .get("name")
- .map(|name| name.clone())
- .unwrap_or("Unknown".to_string());
+ let name = query.get("name").cloned().unwrap_or("Unknown".to_string());
let passcode = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.subsec_nanos())
@@ -119,6 +117,15 @@ async fn create_session(
(StatusCode::CREATED, passcode).into_response()
}
+async fn serve_hands(Path(id): Path<String>, State(state): State<Arc<AppState>>) -> Response {
+ let sessions = state.sessions.lock().unwrap();
+
+ match sessions.get(&id) {
+ Some(session) => Json(session.hands.keys().collect::<Vec<_>>()).into_response(),
+ None => (StatusCode::NOT_FOUND, "Session does not exist").into_response(),
+ }
+}
+
async fn update_hands(
Path(id): Path<String>,
State(state): State<Arc<AppState>>,
@@ -134,3 +141,14 @@ async fn update_hands(
None => (StatusCode::NOT_FOUND, "Session does not exist").into_response(),
}
}
+
+async fn upgrade_play(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> Response {
+ ws.on_upgrade(|socket| handle_play(socket, state))
+}
+
+#[allow(unused_variables)]
+async fn handle_play(mut socket: WebSocket, state: Arc<AppState>) {
+ while let Some(msg) = socket.recv().await {
+ todo!()
+ }
+}
diff --git a/src/session.rs b/src/session.rs
index 11dd5b8..c105af3 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -7,12 +7,18 @@ pub struct Session {
pub hands: HashMap<String, Vec<HandObject>>,
}
+// TODO: The values on these variants will be used in the future and there will be more variants.
+// Once this happens, the dead_code lint should no longer be suppressed.
#[derive(Deserialize)]
+#[allow(dead_code)]
pub enum HandObject {
CustomDeck(CustomDeck),
}
+// TODO: These fields will be used in the future. When they are, the dead_code lint should no longer
+// be suppressed.
#[derive(Deserialize)]
+#[allow(dead_code)]
pub struct CustomDeck {
/// The path/URL of the face cardsheet.
face: String,
diff --git a/src/template.rs b/src/template.rs
index 5424883..8a36a09 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -11,5 +11,6 @@ pub struct IndexTemplate<'a> {
#[derive(Template)]
#[template(path = "session.html")]
pub struct SessionTemplate<'a> {
+ pub id: &'a String,
pub session: &'a Session,
}
diff --git a/templates/session.html b/templates/session.html
index 6319d05..9133df6 100644
--- a/templates/session.html
+++ b/templates/session.html
@@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8">
<title>{{session.steam_name}}'s game</title>
- <script src="/dist/session.js" defer></script>
+ <script id="session-script" data-id="{{id}}" src="/dist/session.js" defer></script>
</head>
<body>
<h1>{{session.steam_name}}'s game</h1>
<form id="player-selection">
<h2>Player Selection</h2>
<label for="color-select">Your color:</label>
- <select id="color-select">
+ <select id="color-select" disabled>
<option value="">None selected</option>
<hr>
{% for (color, _) in session.hands %}