an http based roguelike rpg about jelqing
Blame: src/http.rs
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 1 | use std::{ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 2 | collections::HashMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 3 | convert::Infallible, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 4 | fmt::Write as _, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 5 | sync::{Arc, Mutex}, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 6 | time::Instant, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 7 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 8 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 9 | use axum::{ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 10 | Json, Router, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 11 | body::{Body, Bytes}, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 12 | extract::{Path, Query, Request, State}, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 13 | http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri, Version, header}, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 14 | middleware::{self, Next}, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 15 | response::{IntoResponse, Response}, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 16 | routing::any, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 17 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 18 | use rand::Rng; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 19 | use serde_json::{Value, json}; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 20 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 21 | use crate::{ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 22 | game::{ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 23 | EXCUSES, LUBRICANTS, Lube, MAX_STAMINA, MM_PER_INCH, PatchOp, Run, SUSPICION_LIMIT, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 24 | Technique, shaft, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 25 | }, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 26 | problem::{PROBLEM_BASE, Problem}, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 27 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 28 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 29 | const REL_BASE: &str = "https://jelq.dev/rels/"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 30 | const COOKIE_ATTRIBUTES: &str = "Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 31 | const THERAPY: &str = "Your options are listed in the Allow header. There are more of them than you think. You could also try being honest. It is not in the Allow header because it is not a method."; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 32 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 33 | #[derive(Default)] |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 34 | pub struct AppState { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 35 | runs: Mutex<HashMap<String, Run>>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 36 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 37 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 38 | impl AppState { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 39 | pub fn insert(&self, run: Run) { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 40 | self.runs.lock().unwrap().insert(run.id.clone(), run); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 41 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 42 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 43 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 44 | type Shared = Arc<AppState>; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 45 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 46 | pub fn app() -> Router { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 47 | router(Arc::default()) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 48 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 49 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 50 | pub fn router(state: Shared) -> Router { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 51 | Router::new() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 52 | .route("/runs", any(runs)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 53 | .route("/runs/{id}", any(run)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 54 | .route("/runs/{id}/length", any(length)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 55 | .route("/runs/{id}/foreskin", any(foreskin)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 56 | .route("/runs/{id}/inventory", any(inventory)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 57 | .route("/runs/{id}/inventory/{item}", any(item)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 58 | .route("/runs/{id}/techniques", any(techniques)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 59 | .route("/runs/{id}/drawer", any(drawer)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 60 | .route("/runs/{id}/branches/{choice}", any(branch)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 61 | .route("/runs/{id}/diagnosis", any(diagnosis)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 62 | .route("/graveyard", any(graveyard)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 63 | .route("/leaderboard", any(leaderboard)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 64 | .fallback(not_found) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 65 | .layer(middleware::from_fn(chrome)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 66 | .with_state(state) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 67 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 68 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 69 | async fn chrome(request: Request, next: Next) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 70 | let mut response = next.run(request).await; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 71 | let headers = response.headers_mut(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 72 | headers.insert(header::SERVER, HeaderValue::from_static("jelqd/0.6.9")); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 73 | headers.insert("x-powered-by", HeaderValue::from_static("blood")); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 74 | headers.insert( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 75 | header::CACHE_CONTROL, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 76 | HeaderValue::from_static("no-store, private"), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 77 | ); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 78 | response |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 79 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 80 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 81 | fn header_str(headers: &HeaderMap, name: HeaderName) -> Option<&str> { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 82 | headers.get(name).and_then(|v| v.to_str().ok()) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 83 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 84 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 85 | fn cookie<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 86 | headers |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 87 | .get_all(header::COOKIE) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 88 | .iter() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 89 | .filter_map(|v| v.to_str().ok()) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 90 | .flat_map(|v| v.split(';')) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 91 | .filter_map(|pair| pair.trim().split_once('=')) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 92 | .find(|(key, _)| *key == name) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 93 | .map(|(_, value)| value) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 94 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 95 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 96 | fn unsupported(method: &Method, allow: &'static str) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 97 | if *method == Method::CONNECT { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 98 | return StatusCode::NOT_IMPLEMENTED.into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 99 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 100 | let known = matches!( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 101 | method.as_str(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 102 | "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ" |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 103 | ); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 104 | if !known { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 105 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 106 | StatusCode::NOT_IMPLEMENTED, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 107 | "not-implemented", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 108 | format!("The server does not implement {method}."), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 109 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 110 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 111 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 112 | Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 113 | StatusCode::METHOD_NOT_ALLOWED, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 114 | "method-not-allowed", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 115 | format!("{method} is not supported here."), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 116 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 117 | .header(header::ALLOW, allow) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 118 | .into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 119 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 120 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 121 | fn options(allow: &'static str, patchable: bool) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 122 | let mut response = ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 123 | StatusCode::OK, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 124 | [(header::ALLOW, allow)], |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 125 | Json(json!({ "detail": THERAPY })), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 126 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 127 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 128 | if patchable { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 129 | response.headers_mut().insert( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 130 | "accept-patch", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 131 | HeaderValue::from_static("application/json-patch+json"), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 132 | ); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 133 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 134 | response |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 135 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 136 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 137 | fn trace(method: &Method, uri: &Uri, version: Version, headers: &HeaderMap) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 138 | let mut message = format!("{method} {uri} {version:?}\r\n"); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 139 | for (name, value) in headers { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 140 | let _ = write!( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 141 | message, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 142 | "{name}: {}\r\n", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 143 | value.to_str().unwrap_or("<binary>") |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 144 | ); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 145 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 146 | message.push_str("\r\n"); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 147 | ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 148 | StatusCode::OK, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 149 | [(header::CONTENT_TYPE, "message/http")], |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 150 | message, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 151 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 152 | .into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 153 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 154 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 155 | fn links(run: &Run) -> String { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 156 | let id = &run.id; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 157 | let mut links = vec![format!("</runs/{id}>; rel=\"{REL_BASE}run\"")]; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 158 | for rel in [ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 159 | "length", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 160 | "foreskin", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 161 | "inventory", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 162 | "techniques", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 163 | "drawer", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 164 | "diagnosis", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 165 | ] { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 166 | links.push(format!("</runs/{id}/{rel}>; rel=\"{REL_BASE}{rel}\"")); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 167 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 168 | links.join(", ") |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 169 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 170 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 171 | fn finish(run: &Run, mut response: Response) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 172 | if response.status() == StatusCode::NOT_MODIFIED { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 173 | return response; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 174 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 175 | let mut stats = vec![ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 176 | ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 177 | "x-self-reported-length", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 178 | format!("{:.2}", run.self_reported()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 179 | ), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 180 | ("x-girth", format!("{:.2}", run.girth)), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 181 | ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 182 | "x-foreskin", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 183 | run.foreskin.map_or("gone".into(), |f| format!("{f:.2}")), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 184 | ), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 185 | ("x-eq", run.eq.to_string()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 186 | ("x-stamina", format!("{}/{MAX_STAMINA}", run.stamina)), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 187 | ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 188 | "x-suspicion", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 189 | format!("{}/{SUSPICION_LIMIT}", run.suspicion), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 190 | ), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 191 | ("x-shame", run.total_shame().to_string()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 192 | ("link", links(run)), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 193 | ]; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 194 | if run.observed { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 195 | stats.push(("x-observed-by", "cat".into())); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 196 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 197 | if run.mom_present { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 198 | stats.push(("via", "1.1 mom".into())); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 199 | stats.push(("vary", "Mom".into())); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 200 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 201 | // RFC 9111 obsoleted Warning, which is why nobody heeds it. |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 202 | if run.eq > 70 && run.ended.is_none() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 203 | stats.push(("warning", "199 - \"it is turning purple\"".into())); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 204 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 205 | stats.push(( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 206 | "set-cookie", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 207 | format!("shame={}; {COOKIE_ATTRIBUTES}", run.total_shame()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 208 | )); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 209 | if run.ruler { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 210 | stats.push(("set-cookie", format!("ruler=1; {COOKIE_ATTRIBUTES}"))); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 211 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 212 | let headers = response.headers_mut(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 213 | for (name, value) in stats { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 214 | headers.append(name, HeaderValue::from_str(&value).expect("visible ascii")); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 215 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 216 | response |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 217 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 218 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 219 | fn gone(run: &Run) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 220 | let ending = run.ended.as_ref().expect("only called on ended runs"); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 221 | Problem::new(StatusCode::GONE, "run-ended", "This run has ended.") |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 222 | .with("cause", format!("{PROBLEM_BASE}{}", ending.cause)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 223 | .with("turn", ending.turn) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 224 | .with("graveyard", "/graveyard") |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 225 | .into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 226 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 227 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 228 | fn with_run( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 229 | state: &AppState, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 230 | id: &str, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 231 | headers: &HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 232 | mom_applies: bool, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 233 | handle: impl FnOnce(&mut Run) -> Response, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 234 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 235 | let mut runs = state.runs.lock().unwrap(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 236 | let Some(run) = runs.get_mut(id) else { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 237 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 238 | StatusCode::NOT_FOUND, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 239 | "no-such-run", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 240 | format!("There is no run {id}. There may never have been."), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 241 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 242 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 243 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 244 | if run.ended.is_some() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 245 | return gone(run); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 246 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 247 | run.observed = false; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 248 | if mom_applies |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 249 | && run.mom_present |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 250 | && let Err(problem) = run.answer_door(header_str(headers, header::AUTHORIZATION)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 251 | { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 252 | return finish(run, problem.into_response()); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 253 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 254 | let response = handle(run); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 255 | finish(run, response) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 256 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 257 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 258 | fn respond<T: IntoResponse, E: IntoResponse>(result: Result<T, E>) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 259 | match result { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 260 | Ok(ok) => ok.into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 261 | Err(err) => err.into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 262 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 263 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 264 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 265 | async fn runs( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 266 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 267 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 268 | Query(query): Query<HashMap<String, String>>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 269 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 270 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 271 | if method != Method::POST { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 272 | return unsupported(&method, "POST"); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 273 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 274 | let seed = match query.get("seed").map(|s| s.parse::<u64>()) { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 275 | None => rand::rng().random(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 276 | Some(Ok(seed)) => seed, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 277 | Some(Err(_)) => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 278 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 279 | StatusCode::BAD_REQUEST, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 280 | "bad-seed", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 281 | "The seed must be an unsigned 64-bit integer.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 282 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 283 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 284 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 285 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 286 | let shame = cookie(&headers, "shame") |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 287 | .and_then(|s| s.parse().ok()) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 288 | .unwrap_or(0); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 289 | let id = format!("{:08x}", rand::rng().random::<u32>()); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 290 | let run = Run::new(id.clone(), seed, shame); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 291 | let response = ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 292 | StatusCode::CREATED, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 293 | [ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 294 | (header::LOCATION, format!("/runs/{id}")), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 295 | (header::ETAG, run.etag()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 296 | (HeaderName::from_static("x-seed"), seed.to_string()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 297 | ], |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 298 | Json(run.view()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 299 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 300 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 301 | let response = finish(&run, response); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 302 | state.insert(run); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 303 | response |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 304 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 305 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 306 | #[allow(clippy::too_many_arguments)] |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 307 | async fn run( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 308 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 309 | Path(id): Path<String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 310 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 311 | uri: Uri, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 312 | version: Version, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 313 | Query(query): Query<HashMap<String, String>>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 314 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 315 | body: Bytes, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 316 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 317 | const ALLOW: &str = "GET, HEAD, JELQ, PUT, PATCH, DELETE, OPTIONS, TRACE"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 318 | let now = Instant::now(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 319 | with_run(&state, &id, &headers, true, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 320 | "GET" | "HEAD" => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 321 | let accept = header_str(&headers, header::ACCEPT).unwrap_or_default(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 322 | if accept.contains("reassurance") { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 323 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 324 | StatusCode::NOT_ACCEPTABLE, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 325 | "no-reassurance", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 326 | "The server does not produce reassurance.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 327 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 328 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 329 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 330 | ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 331 | StatusCode::OK, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 332 | [(header::ETAG, run.etag())], |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 333 | Json(run.view()), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 334 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 335 | .into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 336 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 337 | "JELQ" => jelq(run, &query, &headers, now), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 338 | "PUT" => run.put().into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 339 | "PATCH" => patch(run, &headers, &body), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 340 | "DELETE" => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 341 | run.end("abandoned", "The run was abandoned."); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 342 | StatusCode::NO_CONTENT.into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 343 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 344 | "OPTIONS" => options(ALLOW, true), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 345 | "TRACE" => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 346 | run.shame += 1; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 347 | trace(&method, &uri, version, &headers) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 348 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 349 | _ => unsupported(&method, ALLOW), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 350 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 351 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 352 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 353 | fn jelq( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 354 | run: &mut Run, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 355 | query: &HashMap<String, String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 356 | headers: &HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 357 | now: Instant, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 358 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 359 | let technique = match query.get("technique") { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 360 | None => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 361 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 362 | StatusCode::BAD_REQUEST, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 363 | "technique-required", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 364 | "No technique was specified.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 365 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 366 | .with("techniques", format!("/runs/{}/techniques", run.id)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 367 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 368 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 369 | Some(name) => match Technique::parse(name) { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 370 | Some(t) => t, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 371 | None => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 372 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 373 | StatusCode::BAD_REQUEST, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 374 | "unknown-technique", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 375 | format!("{name} is not a technique."), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 376 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 377 | .with("techniques", format!("/runs/{}/techniques", run.id)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 378 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 379 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 380 | }, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 381 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 382 | let intensity = match query.get("intensity").map(|i| i.parse::<i32>()) { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 383 | None => 5, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 384 | Some(Ok(i)) if (1..=10).contains(&i) => i, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 385 | Some(_) => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 386 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 387 | StatusCode::BAD_REQUEST, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 388 | "intensity-out-of-range", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 389 | "Intensity is measured from 1 to 10.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 390 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 391 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 392 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 393 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 394 | let lube = match Lube::from_content_type(header_str(headers, header::CONTENT_TYPE)) { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 395 | Ok(lube) => lube, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 396 | Err(problem) => return problem.into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 397 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 398 | respond(run.jelq(technique, intensity, lube, now)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 399 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 400 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 401 | fn patch(run: &mut Run, headers: &HeaderMap, body: &[u8]) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 402 | let content_type = header_str(headers, header::CONTENT_TYPE).unwrap_or_default(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 403 | if !content_type.starts_with("application/json-patch+json") { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 404 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 405 | StatusCode::UNSUPPORTED_MEDIA_TYPE, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 406 | "patch-format", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 407 | "Patches are accepted as application/json-patch+json.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 408 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 409 | .header( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 410 | HeaderName::from_static("accept-patch"), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 411 | "application/json-patch+json", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 412 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 413 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 414 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 415 | match serde_json::from_slice::<Vec<PatchOp>>(body) { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 416 | Ok(ops) => respond(run.patch(&ops)), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 417 | Err(_) => Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 418 | StatusCode::BAD_REQUEST, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 419 | "malformed-patch", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 420 | "The patch could not be parsed.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 421 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 422 | .into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 423 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 424 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 425 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 426 | async fn length( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 427 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 428 | Path(id): Path<String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 429 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 430 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 431 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 432 | const ALLOW: &str = "GET, HEAD, OPTIONS"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 433 | with_run(&state, &id, &headers, true, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 434 | "GET" | "HEAD" => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 435 | let content_type = [(header::CONTENT_TYPE, "text/plain; charset=us-ascii")]; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 436 | if run.ruler { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 437 | let mm = (run.length * MM_PER_INCH).round() as usize; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 438 | return (StatusCode::OK, content_type, shaft(mm)).into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 439 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 440 | let mm = (run.self_reported() * MM_PER_INCH).round() as usize; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 441 | let chunks: Vec<Result<Bytes, Infallible>> = shaft(mm) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 442 | .into_bytes() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 443 | .chunks(16) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 444 | .map(|c| Ok(Bytes::copy_from_slice(c))) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 445 | .collect(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 446 | let body = Body::from_stream(futures_util::stream::iter(chunks)); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 447 | (StatusCode::OK, content_type, body).into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 448 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 449 | "OPTIONS" => options(ALLOW, false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 450 | _ => unsupported(&method, ALLOW), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 451 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 452 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 453 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 454 | async fn foreskin( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 455 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 456 | Path(id): Path<String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 457 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 458 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 459 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 460 | const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 461 | with_run(&state, &id, &headers, true, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 462 | "GET" | "HEAD" => match run.foreskin { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 463 | Some(f) => ( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 464 | StatusCode::OK, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 465 | [(header::ETAG, run.etag())], |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 466 | Json(json!({ "foreskin": (f * 100.0).round() / 100.0 })), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 467 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 468 | .into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 469 | None => Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 470 | StatusCode::GONE, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 471 | "foreskin-gone", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 472 | "The foreskin is gone. It will not be returning.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 473 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 474 | .into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 475 | }, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 476 | "DELETE" => respond(run.delete_foreskin(header_str(&headers, header::IF_MATCH))), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 477 | "OPTIONS" => options(ALLOW, false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 478 | _ => unsupported(&method, ALLOW), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 479 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 480 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 481 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 482 | fn items(run: &Run) -> Vec<(&'static str, &'static str)> { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 483 | let mut items = vec![( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 484 | "warm-towel", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 485 | "Reusable. Satisfies the warm-up precondition. Use technique=warmup.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 486 | )]; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 487 | if run.ice_packs > 0 { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 488 | items.push(( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 489 | "ice-pack", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 490 | "Lowers EQ by 40. Use technique=ice. There is one.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 491 | )); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 492 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 493 | if run.ruler { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 494 | items.push(( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 495 | "ruler", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 496 | "Measures honestly. The length resource now carries a Content-Length.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 497 | )); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 498 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 499 | if run.teapot { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 500 | items.push(("teapot-spout", "Does nothing. Cannot be dropped.")); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 501 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 502 | items |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 503 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 504 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 505 | async fn inventory( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 506 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 507 | Path(id): Path<String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 508 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 509 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 510 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 511 | const ALLOW: &str = "GET, HEAD, OPTIONS"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 512 | with_run(&state, &id, &headers, false, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 513 | "GET" | "HEAD" => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 514 | let listed: Vec<Value> = items(run) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 515 | .into_iter() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 516 | .map(|(name, _)| json!({ "name": name, "href": format!("/runs/{}/inventory/{name}", run.id) })) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 517 | .collect(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 518 | Json(json!({ "items": listed, "excuses": EXCUSES })).into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 519 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 520 | "OPTIONS" => options(ALLOW, false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 521 | _ => unsupported(&method, ALLOW), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 522 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 523 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 524 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 525 | async fn item( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 526 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 527 | Path((id, name)): Path<(String, String)>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 528 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 529 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 530 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 531 | const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 532 | with_run(&state, &id, &headers, false, |run| { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 533 | let Some((name, description)) = items(run).into_iter().find(|(n, _)| *n == name) else { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 534 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 535 | StatusCode::NOT_FOUND, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 536 | "no-such-item", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 537 | "You do not have that.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 538 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 539 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 540 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 541 | match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 542 | "GET" | "HEAD" => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 543 | Json(json!({ "name": name, "description": description })).into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 544 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 545 | "DELETE" => Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 546 | StatusCode::FORBIDDEN, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 547 | "cannot-be-dropped", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 548 | "Items cannot be dropped.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 549 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 550 | .into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 551 | "OPTIONS" => options(ALLOW, false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 552 | _ => unsupported(&method, ALLOW), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 553 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 554 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 555 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 556 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 557 | async fn techniques( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 558 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 559 | Path(id): Path<String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 560 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 561 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 562 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 563 | const ALLOW: &str = "GET, HEAD, OPTIONS"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 564 | with_run(&state, &id, &headers, true, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 565 | "GET" | "HEAD" => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 566 | let listed: Vec<Value> = Technique::ALL |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 567 | .into_iter() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 568 | .map(|t| t.describe(run.forum_read)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 569 | .collect(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 570 | Json(json!({ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 571 | "usage": format!("JELQ /runs/{}?technique=<name>&intensity=<1-10>", run.id), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 572 | "lubricant": "Declared with Content-Type.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 573 | "lubricants": LUBRICANTS, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 574 | "eq_band": [50, 70], |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 575 | "techniques": listed, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 576 | })) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 577 | .into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 578 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 579 | "OPTIONS" => options(ALLOW, false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 580 | _ => unsupported(&method, ALLOW), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 581 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 582 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 583 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 584 | async fn drawer( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 585 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 586 | Path(id): Path<String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 587 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 588 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 589 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 590 | with_run(&state, &id, &headers, true, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 591 | "POST" => run.search_drawer().into_response(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 592 | "OPTIONS" => options("POST, OPTIONS", false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 593 | _ => unsupported(&method, "POST, OPTIONS"), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 594 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 595 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 596 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 597 | async fn branch( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 598 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 599 | Path((id, choice)): Path<(String, String)>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 600 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 601 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 602 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 603 | with_run(&state, &id, &headers, true, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 604 | "POST" => respond(run.choose(&choice)), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 605 | "OPTIONS" => options("POST, OPTIONS", false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 606 | _ => unsupported(&method, "POST, OPTIONS"), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 607 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 608 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 609 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 610 | async fn diagnosis( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 611 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 612 | Path(id): Path<String>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 613 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 614 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 615 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 616 | const ALLOW: &str = "GET, HEAD, OPTIONS"; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 617 | with_run(&state, &id, &headers, false, |run| match method.as_str() { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 618 | "GET" | "HEAD" => respond(run.diagnose( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 619 | header_str(&headers, header::AUTHORIZATION), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 620 | header_str(&headers, header::IF_NONE_MATCH), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 621 | )), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 622 | "OPTIONS" => options(ALLOW, false), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 623 | _ => unsupported(&method, ALLOW), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 624 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 625 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 626 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 627 | async fn graveyard(State(state): State<Shared>, method: Method) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 628 | if method != Method::GET && method != Method::HEAD { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 629 | return unsupported(&method, "GET, HEAD"); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 630 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 631 | let runs = state.runs.lock().unwrap(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 632 | let dead: Vec<Value> = runs |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 633 | .values() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 634 | .filter_map(|run| { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 635 | let ending = run.ended.as_ref()?; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 636 | Some(json!({ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 637 | "id": run.id, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 638 | "cause": format!("{PROBLEM_BASE}{}", ending.cause), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 639 | "detail": ending.detail, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 640 | "turn": ending.turn, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 641 | })) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 642 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 643 | .collect(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 644 | Json(json!({ "runs": dead })).into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 645 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 646 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 647 | async fn leaderboard( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 648 | State(state): State<Shared>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 649 | method: Method, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 650 | Query(query): Query<HashMap<String, String>>, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 651 | headers: HeaderMap, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 652 | ) -> Response { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 653 | if method != Method::GET && method != Method::HEAD { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 654 | return unsupported(&method, "GET, HEAD"); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 655 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 656 | let truthful = match query.get("sort").map(String::as_str) { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 657 | None | Some("self-reported-length") => false, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 658 | Some("content-length") if cookie(&headers, "ruler") == Some("1") => true, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 659 | Some("content-length") => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 660 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 661 | StatusCode::FORBIDDEN, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 662 | "ruler-required", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 663 | "Sorting by the truth requires a Ruler.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 664 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 665 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 666 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 667 | Some(_) => { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 668 | return Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 669 | StatusCode::BAD_REQUEST, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 670 | "unknown-sort", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 671 | "The leaderboard sorts by self-reported-length or content-length.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 672 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 673 | .into_response(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 674 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 675 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 676 | let runs = state.runs.lock().unwrap(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 677 | let mut entries: Vec<&Run> = runs.values().collect(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 678 | entries.sort_by(|a, b| { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 679 | let key = |r: &Run| { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 680 | if truthful { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 681 | r.length |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 682 | } else { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 683 | r.self_reported() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 684 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 685 | }; |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 686 | key(b).total_cmp(&key(a)) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 687 | }); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 688 | let entries: Vec<Value> = entries |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 689 | .into_iter() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 690 | .map(|run| { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 691 | let mut entry = json!({ |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 692 | "id": run.id, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 693 | "status": run.status(), |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 694 | "self_reported_length": (run.self_reported() * 100.0).round() / 100.0, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 695 | }); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 696 | if truthful { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 697 | entry["content_length"] = json!((run.length * MM_PER_INCH).round() as u64); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 698 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 699 | entry |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 700 | }) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 701 | .collect(); |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 702 | Json(json!({ "entries": entries })).into_response() |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 703 | } |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 704 | |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 705 | async fn not_found() -> Problem { |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 706 | Problem::new( |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 707 | StatusCode::NOT_FOUND, |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 708 | "not-found", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 709 | "The resource does not exist. This is not about you.", |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 710 | ) |
a9b016e4b248 | atagen | 2026-10-10 08:12 UTC | 711 | } |