atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
dfa38cf4cd4b9978f5ad92f39b2c735870aecaba
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/http.rs

a9b016e4b248atagen2026-10-10 08:12 UTC1
use std::{
a9b016e4b248atagen2026-10-10 08:12 UTC2
    collections::HashMap,
a9b016e4b248atagen2026-10-10 08:12 UTC3
    convert::Infallible,
a9b016e4b248atagen2026-10-10 08:12 UTC4
    fmt::Write as _,
dfa38cf4cd4batagen2026-10-10 09:17 UTC5
    io::Write as _,
a9b016e4b248atagen2026-10-10 08:12 UTC6
    sync::{Arc, Mutex},
a9b016e4b248atagen2026-10-10 08:12 UTC7
    time::Instant,
a9b016e4b248atagen2026-10-10 08:12 UTC8
};
a9b016e4b248atagen2026-10-10 08:12 UTC9
a9b016e4b248atagen2026-10-10 08:12 UTC10
use axum::{
a9b016e4b248atagen2026-10-10 08:12 UTC11
    Json, Router,
a9b016e4b248atagen2026-10-10 08:12 UTC12
    body::{Body, Bytes},
a9b016e4b248atagen2026-10-10 08:12 UTC13
    extract::{Path, Query, Request, State},
a9b016e4b248atagen2026-10-10 08:12 UTC14
    http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri, Version, header},
a9b016e4b248atagen2026-10-10 08:12 UTC15
    middleware::{self, Next},
a9b016e4b248atagen2026-10-10 08:12 UTC16
    response::{IntoResponse, Response},
a9b016e4b248atagen2026-10-10 08:12 UTC17
    routing::any,
a9b016e4b248atagen2026-10-10 08:12 UTC18
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC19
use flate2::{Compression, write::GzEncoder};
dfa38cf4cd4batagen2026-10-10 09:17 UTC20
use hyper_util::rt::TokioIo;
a9b016e4b248atagen2026-10-10 08:12 UTC21
use rand::Rng;
a9b016e4b248atagen2026-10-10 08:12 UTC22
use serde_json::{Value, json};
dfa38cf4cd4batagen2026-10-10 09:17 UTC23
use tokio::io::AsyncWriteExt;
a9b016e4b248atagen2026-10-10 08:12 UTC24
a9b016e4b248atagen2026-10-10 08:12 UTC25
use crate::{
a9b016e4b248atagen2026-10-10 08:12 UTC26
    game::{
dfa38cf4cd4batagen2026-10-10 09:17 UTC27
        ELDER_LAST_MODIFIED, FORUM_THREAD, Floor, Grip, LUBRICANTS, Lube, MAX_STAMINA, MM_PER_INCH,
dfa38cf4cd4batagen2026-10-10 09:17 UTC28
        PatchOp, RUN_ALLOW, Run, SUSPICION_LIMIT, Technique, forum_thread, shaft,
a9b016e4b248atagen2026-10-10 08:12 UTC29
    },
a9b016e4b248atagen2026-10-10 08:12 UTC30
    problem::{PROBLEM_BASE, Problem},
a9b016e4b248atagen2026-10-10 08:12 UTC31
};
a9b016e4b248atagen2026-10-10 08:12 UTC32
a9b016e4b248atagen2026-10-10 08:12 UTC33
const REL_BASE: &str = "https://jelq.dev/rels/";
a9b016e4b248atagen2026-10-10 08:12 UTC34
const COOKIE_ATTRIBUTES: &str = "Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict";
a9b016e4b248atagen2026-10-10 08:12 UTC35
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.";
dfa38cf4cd4batagen2026-10-10 09:17 UTC36
const MAX_URI: usize = 512;
dfa38cf4cd4batagen2026-10-10 09:17 UTC37
const MAX_HEADER: usize = 1024;
a9b016e4b248atagen2026-10-10 08:12 UTC38
a9b016e4b248atagen2026-10-10 08:12 UTC39
#[derive(Default)]
a9b016e4b248atagen2026-10-10 08:12 UTC40
pub struct AppState {
a9b016e4b248atagen2026-10-10 08:12 UTC41
    runs: Mutex<HashMap<String, Run>>,
a9b016e4b248atagen2026-10-10 08:12 UTC42
}
a9b016e4b248atagen2026-10-10 08:12 UTC43
a9b016e4b248atagen2026-10-10 08:12 UTC44
impl AppState {
a9b016e4b248atagen2026-10-10 08:12 UTC45
    pub fn insert(&self, run: Run) {
a9b016e4b248atagen2026-10-10 08:12 UTC46
        self.runs.lock().unwrap().insert(run.id.clone(), run);
a9b016e4b248atagen2026-10-10 08:12 UTC47
    }
a9b016e4b248atagen2026-10-10 08:12 UTC48
}
a9b016e4b248atagen2026-10-10 08:12 UTC49
a9b016e4b248atagen2026-10-10 08:12 UTC50
type Shared = Arc<AppState>;
a9b016e4b248atagen2026-10-10 08:12 UTC51
dfa38cf4cd4batagen2026-10-10 09:17 UTC52
#[derive(Clone)]
dfa38cf4cd4batagen2026-10-10 09:17 UTC53
struct Compress;
dfa38cf4cd4batagen2026-10-10 09:17 UTC54
dfa38cf4cd4batagen2026-10-10 09:17 UTC55
/// Which household hazards stand between the request and the run.
dfa38cf4cd4batagen2026-10-10 09:17 UTC56
#[derive(Clone, Copy, PartialEq, Eq)]
dfa38cf4cd4batagen2026-10-10 09:17 UTC57
enum Scope {
dfa38cf4cd4batagen2026-10-10 09:17 UTC58
    Home,
dfa38cf4cd4batagen2026-10-10 09:17 UTC59
    Pockets,
dfa38cf4cd4batagen2026-10-10 09:17 UTC60
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC61
a9b016e4b248atagen2026-10-10 08:12 UTC62
pub fn app() -> Router {
a9b016e4b248atagen2026-10-10 08:12 UTC63
    router(Arc::default())
a9b016e4b248atagen2026-10-10 08:12 UTC64
}
a9b016e4b248atagen2026-10-10 08:12 UTC65
a9b016e4b248atagen2026-10-10 08:12 UTC66
pub fn router(state: Shared) -> Router {
a9b016e4b248atagen2026-10-10 08:12 UTC67
    Router::new()
a9b016e4b248atagen2026-10-10 08:12 UTC68
        .route("/runs", any(runs))
a9b016e4b248atagen2026-10-10 08:12 UTC69
        .route("/runs/{id}", any(run))
a9b016e4b248atagen2026-10-10 08:12 UTC70
        .route("/runs/{id}/length", any(length))
a9b016e4b248atagen2026-10-10 08:12 UTC71
        .route("/runs/{id}/foreskin", any(foreskin))
a9b016e4b248atagen2026-10-10 08:12 UTC72
        .route("/runs/{id}/inventory", any(inventory))
a9b016e4b248atagen2026-10-10 08:12 UTC73
        .route("/runs/{id}/inventory/{item}", any(item))
a9b016e4b248atagen2026-10-10 08:12 UTC74
        .route("/runs/{id}/techniques", any(techniques))
a9b016e4b248atagen2026-10-10 08:12 UTC75
        .route("/runs/{id}/drawer", any(drawer))
a9b016e4b248atagen2026-10-10 08:12 UTC76
        .route("/runs/{id}/branches/{choice}", any(branch))
dfa38cf4cd4batagen2026-10-10 09:17 UTC77
        .route("/runs/{id}/floor", any(floor))
dfa38cf4cd4batagen2026-10-10 09:17 UTC78
        .route("/runs/{id}/shop", any(shop))
dfa38cf4cd4batagen2026-10-10 09:17 UTC79
        .route("/runs/{id}/shop/{item}", any(buy))
dfa38cf4cd4batagen2026-10-10 09:17 UTC80
        .route("/runs/{id}/portal", any(portal))
dfa38cf4cd4batagen2026-10-10 09:17 UTC81
        .route("/runs/{id}/elder", any(elder))
dfa38cf4cd4batagen2026-10-10 09:17 UTC82
        .route("/runs/{id}/grip", any(grip))
a9b016e4b248atagen2026-10-10 08:12 UTC83
        .route("/runs/{id}/diagnosis", any(diagnosis))
dfa38cf4cd4batagen2026-10-10 09:17 UTC84
        .route("/forum/threads/{year}", any(thread))
a9b016e4b248atagen2026-10-10 08:12 UTC85
        .route("/graveyard", any(graveyard))
a9b016e4b248atagen2026-10-10 08:12 UTC86
        .route("/leaderboard", any(leaderboard))
a9b016e4b248atagen2026-10-10 08:12 UTC87
        .fallback(not_found)
a9b016e4b248atagen2026-10-10 08:12 UTC88
        .layer(middleware::from_fn(chrome))
a9b016e4b248atagen2026-10-10 08:12 UTC89
        .with_state(state)
a9b016e4b248atagen2026-10-10 08:12 UTC90
}
a9b016e4b248atagen2026-10-10 08:12 UTC91
dfa38cf4cd4batagen2026-10-10 09:17 UTC92
fn reject(request: &Request) -> Option<Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC93
    if request.version() < Version::HTTP_11 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC94
        return Some(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC95
            StatusCode::HTTP_VERSION_NOT_SUPPORTED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC96
            "pdf",
dfa38cf4cd4batagen2026-10-10 09:17 UTC97
            "HTTP/1.0 is the version the original jelqing PDF was written for. The server has read it.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC98
        ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC99
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC100
    if request.uri().to_string().len() > MAX_URI {
dfa38cf4cd4batagen2026-10-10 09:17 UTC101
        return Some(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC102
            StatusCode::URI_TOO_LONG,
dfa38cf4cd4batagen2026-10-10 09:17 UTC103
            "uri-too-long",
dfa38cf4cd4batagen2026-10-10 09:17 UTC104
            "The URI is longer than anything else here.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC105
        ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC106
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC107
    if request.headers().values().any(|v| v.len() > MAX_HEADER) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC108
        return Some(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC109
            StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC110
            "head-too-large",
dfa38cf4cd4batagen2026-10-10 09:17 UTC111
            "Head too large.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC112
        ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC113
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC114
    let expectation = request.headers().get(header::EXPECT)?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC115
    if expectation.as_bytes().eq_ignore_ascii_case(b"100-continue") {
dfa38cf4cd4batagen2026-10-10 09:17 UTC116
        return None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC117
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC118
    Some(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC119
        StatusCode::EXPECTATION_FAILED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC120
        "expectations",
dfa38cf4cd4batagen2026-10-10 09:17 UTC121
        format!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC122
            "Expect: {} cannot be met.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC123
            expectation.to_str().unwrap_or("that")
dfa38cf4cd4batagen2026-10-10 09:17 UTC124
        ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC125
    ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC126
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC127
dfa38cf4cd4batagen2026-10-10 09:17 UTC128
async fn compress(response: Response) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC129
    let (mut parts, body) = response.into_parts();
dfa38cf4cd4batagen2026-10-10 09:17 UTC130
    let Ok(bytes) = axum::body::to_bytes(body, usize::MAX).await else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC131
        return Response::from_parts(parts, Body::empty());
dfa38cf4cd4batagen2026-10-10 09:17 UTC132
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC133
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
dfa38cf4cd4batagen2026-10-10 09:17 UTC134
    if encoder.write_all(&bytes).is_err() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC135
        return Response::from_parts(parts, Body::from(bytes));
dfa38cf4cd4batagen2026-10-10 09:17 UTC136
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC137
    let Ok(gzipped) = encoder.finish() else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC138
        return Response::from_parts(parts, Body::from(bytes));
dfa38cf4cd4batagen2026-10-10 09:17 UTC139
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC140
    parts
dfa38cf4cd4batagen2026-10-10 09:17 UTC141
        .headers
dfa38cf4cd4batagen2026-10-10 09:17 UTC142
        .insert(header::CONTENT_ENCODING, HeaderValue::from_static("gzip"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC143
    parts.headers.remove(header::CONTENT_LENGTH);
dfa38cf4cd4batagen2026-10-10 09:17 UTC144
    parts
dfa38cf4cd4batagen2026-10-10 09:17 UTC145
        .headers
dfa38cf4cd4batagen2026-10-10 09:17 UTC146
        .append(header::VARY, HeaderValue::from_static("Accept-Encoding"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC147
    Response::from_parts(parts, Body::from(gzipped))
dfa38cf4cd4batagen2026-10-10 09:17 UTC148
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC149
a9b016e4b248atagen2026-10-10 08:12 UTC150
async fn chrome(request: Request, next: Next) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC151
    let gzip = header_str(request.headers(), header::ACCEPT_ENCODING)
dfa38cf4cd4batagen2026-10-10 09:17 UTC152
        .is_some_and(|e| e.to_ascii_lowercase().contains("gzip"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC153
    let mut response = match reject(&request) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC154
        Some(problem) => problem.into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC155
        None => next.run(request).await,
dfa38cf4cd4batagen2026-10-10 09:17 UTC156
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC157
    if gzip && response.extensions().get::<Compress>().is_some() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC158
        response = compress(response).await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC159
    }
a9b016e4b248atagen2026-10-10 08:12 UTC160
    let headers = response.headers_mut();
a9b016e4b248atagen2026-10-10 08:12 UTC161
    headers.insert(header::SERVER, HeaderValue::from_static("jelqd/0.6.9"));
a9b016e4b248atagen2026-10-10 08:12 UTC162
    headers.insert("x-powered-by", HeaderValue::from_static("blood"));
a9b016e4b248atagen2026-10-10 08:12 UTC163
    headers.insert(
a9b016e4b248atagen2026-10-10 08:12 UTC164
        header::CACHE_CONTROL,
a9b016e4b248atagen2026-10-10 08:12 UTC165
        HeaderValue::from_static("no-store, private"),
a9b016e4b248atagen2026-10-10 08:12 UTC166
    );
a9b016e4b248atagen2026-10-10 08:12 UTC167
    response
a9b016e4b248atagen2026-10-10 08:12 UTC168
}
a9b016e4b248atagen2026-10-10 08:12 UTC169
a9b016e4b248atagen2026-10-10 08:12 UTC170
fn header_str(headers: &HeaderMap, name: HeaderName) -> Option<&str> {
a9b016e4b248atagen2026-10-10 08:12 UTC171
    headers.get(name).and_then(|v| v.to_str().ok())
a9b016e4b248atagen2026-10-10 08:12 UTC172
}
a9b016e4b248atagen2026-10-10 08:12 UTC173
a9b016e4b248atagen2026-10-10 08:12 UTC174
fn cookie<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
a9b016e4b248atagen2026-10-10 08:12 UTC175
    headers
a9b016e4b248atagen2026-10-10 08:12 UTC176
        .get_all(header::COOKIE)
a9b016e4b248atagen2026-10-10 08:12 UTC177
        .iter()
a9b016e4b248atagen2026-10-10 08:12 UTC178
        .filter_map(|v| v.to_str().ok())
a9b016e4b248atagen2026-10-10 08:12 UTC179
        .flat_map(|v| v.split(';'))
a9b016e4b248atagen2026-10-10 08:12 UTC180
        .filter_map(|pair| pair.trim().split_once('='))
a9b016e4b248atagen2026-10-10 08:12 UTC181
        .find(|(key, _)| *key == name)
a9b016e4b248atagen2026-10-10 08:12 UTC182
        .map(|(_, value)| value)
a9b016e4b248atagen2026-10-10 08:12 UTC183
}
a9b016e4b248atagen2026-10-10 08:12 UTC184
a9b016e4b248atagen2026-10-10 08:12 UTC185
fn unsupported(method: &Method, allow: &'static str) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC186
    if *method == Method::CONNECT {
a9b016e4b248atagen2026-10-10 08:12 UTC187
        return StatusCode::NOT_IMPLEMENTED.into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC188
    }
a9b016e4b248atagen2026-10-10 08:12 UTC189
    let known = matches!(
a9b016e4b248atagen2026-10-10 08:12 UTC190
        method.as_str(),
a9b016e4b248atagen2026-10-10 08:12 UTC191
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
a9b016e4b248atagen2026-10-10 08:12 UTC192
    );
a9b016e4b248atagen2026-10-10 08:12 UTC193
    if !known {
a9b016e4b248atagen2026-10-10 08:12 UTC194
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC195
            StatusCode::NOT_IMPLEMENTED,
a9b016e4b248atagen2026-10-10 08:12 UTC196
            "not-implemented",
a9b016e4b248atagen2026-10-10 08:12 UTC197
            format!("The server does not implement {method}."),
a9b016e4b248atagen2026-10-10 08:12 UTC198
        )
a9b016e4b248atagen2026-10-10 08:12 UTC199
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC200
    }
a9b016e4b248atagen2026-10-10 08:12 UTC201
    Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC202
        StatusCode::METHOD_NOT_ALLOWED,
a9b016e4b248atagen2026-10-10 08:12 UTC203
        "method-not-allowed",
a9b016e4b248atagen2026-10-10 08:12 UTC204
        format!("{method} is not supported here."),
a9b016e4b248atagen2026-10-10 08:12 UTC205
    )
a9b016e4b248atagen2026-10-10 08:12 UTC206
    .header(header::ALLOW, allow)
a9b016e4b248atagen2026-10-10 08:12 UTC207
    .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC208
}
a9b016e4b248atagen2026-10-10 08:12 UTC209
a9b016e4b248atagen2026-10-10 08:12 UTC210
fn options(allow: &'static str, patchable: bool) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC211
    let mut response = (
a9b016e4b248atagen2026-10-10 08:12 UTC212
        StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC213
        [(header::ALLOW, allow)],
a9b016e4b248atagen2026-10-10 08:12 UTC214
        Json(json!({ "detail": THERAPY })),
a9b016e4b248atagen2026-10-10 08:12 UTC215
    )
a9b016e4b248atagen2026-10-10 08:12 UTC216
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC217
    if patchable {
a9b016e4b248atagen2026-10-10 08:12 UTC218
        response.headers_mut().insert(
a9b016e4b248atagen2026-10-10 08:12 UTC219
            "accept-patch",
a9b016e4b248atagen2026-10-10 08:12 UTC220
            HeaderValue::from_static("application/json-patch+json"),
a9b016e4b248atagen2026-10-10 08:12 UTC221
        );
a9b016e4b248atagen2026-10-10 08:12 UTC222
    }
a9b016e4b248atagen2026-10-10 08:12 UTC223
    response
a9b016e4b248atagen2026-10-10 08:12 UTC224
}
a9b016e4b248atagen2026-10-10 08:12 UTC225
a9b016e4b248atagen2026-10-10 08:12 UTC226
fn trace(method: &Method, uri: &Uri, version: Version, headers: &HeaderMap) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC227
    let mut message = format!("{method} {uri} {version:?}\r\n");
a9b016e4b248atagen2026-10-10 08:12 UTC228
    for (name, value) in headers {
a9b016e4b248atagen2026-10-10 08:12 UTC229
        let _ = write!(
a9b016e4b248atagen2026-10-10 08:12 UTC230
            message,
a9b016e4b248atagen2026-10-10 08:12 UTC231
            "{name}: {}\r\n",
a9b016e4b248atagen2026-10-10 08:12 UTC232
            value.to_str().unwrap_or("<binary>")
a9b016e4b248atagen2026-10-10 08:12 UTC233
        );
a9b016e4b248atagen2026-10-10 08:12 UTC234
    }
a9b016e4b248atagen2026-10-10 08:12 UTC235
    message.push_str("\r\n");
a9b016e4b248atagen2026-10-10 08:12 UTC236
    (
a9b016e4b248atagen2026-10-10 08:12 UTC237
        StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC238
        [(header::CONTENT_TYPE, "message/http")],
a9b016e4b248atagen2026-10-10 08:12 UTC239
        message,
a9b016e4b248atagen2026-10-10 08:12 UTC240
    )
a9b016e4b248atagen2026-10-10 08:12 UTC241
        .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC242
}
a9b016e4b248atagen2026-10-10 08:12 UTC243
a9b016e4b248atagen2026-10-10 08:12 UTC244
fn links(run: &Run) -> String {
a9b016e4b248atagen2026-10-10 08:12 UTC245
    let id = &run.id;
a9b016e4b248atagen2026-10-10 08:12 UTC246
    let mut links = vec![format!("</runs/{id}>; rel=\"{REL_BASE}run\"")];
a9b016e4b248atagen2026-10-10 08:12 UTC247
    for rel in [
dfa38cf4cd4batagen2026-10-10 09:17 UTC248
        "floor",
a9b016e4b248atagen2026-10-10 08:12 UTC249
        "length",
a9b016e4b248atagen2026-10-10 08:12 UTC250
        "foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC251
        "inventory",
a9b016e4b248atagen2026-10-10 08:12 UTC252
        "techniques",
a9b016e4b248atagen2026-10-10 08:12 UTC253
        "drawer",
dfa38cf4cd4batagen2026-10-10 09:17 UTC254
        "grip",
a9b016e4b248atagen2026-10-10 08:12 UTC255
        "diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC256
    ] {
a9b016e4b248atagen2026-10-10 08:12 UTC257
        links.push(format!("</runs/{id}/{rel}>; rel=\"{REL_BASE}{rel}\""));
a9b016e4b248atagen2026-10-10 08:12 UTC258
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC259
    if run.floor.shop_open() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC260
        links.push(format!("</runs/{id}/shop>; rel=\"{REL_BASE}shop\""));
dfa38cf4cd4batagen2026-10-10 09:17 UTC261
    }
a9b016e4b248atagen2026-10-10 08:12 UTC262
    links.join(", ")
a9b016e4b248atagen2026-10-10 08:12 UTC263
}
a9b016e4b248atagen2026-10-10 08:12 UTC264
a9b016e4b248atagen2026-10-10 08:12 UTC265
fn finish(run: &Run, mut response: Response) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC266
    let status = response.status();
dfa38cf4cd4batagen2026-10-10 09:17 UTC267
    if status == StatusCode::NOT_MODIFIED {
a9b016e4b248atagen2026-10-10 08:12 UTC268
        return response;
a9b016e4b248atagen2026-10-10 08:12 UTC269
    }
a9b016e4b248atagen2026-10-10 08:12 UTC270
    let mut stats = vec![
a9b016e4b248atagen2026-10-10 08:12 UTC271
        (
a9b016e4b248atagen2026-10-10 08:12 UTC272
            "x-self-reported-length",
a9b016e4b248atagen2026-10-10 08:12 UTC273
            format!("{:.2}", run.self_reported()),
a9b016e4b248atagen2026-10-10 08:12 UTC274
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC275
        ("x-girth", format!("{:.2}", run.girth)),
a9b016e4b248atagen2026-10-10 08:12 UTC276
        (
a9b016e4b248atagen2026-10-10 08:12 UTC277
            "x-foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC278
            run.foreskin.map_or("gone".into(), |f| format!("{f:.2}")),
a9b016e4b248atagen2026-10-10 08:12 UTC279
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC280
        ("x-eq", run.eq.to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC281
        ("x-stamina", format!("{}/{MAX_STAMINA}", run.stamina)),
a9b016e4b248atagen2026-10-10 08:12 UTC282
        (
a9b016e4b248atagen2026-10-10 08:12 UTC283
            "x-suspicion",
a9b016e4b248atagen2026-10-10 08:12 UTC284
            format!("{}/{SUSPICION_LIMIT}", run.suspicion),
a9b016e4b248atagen2026-10-10 08:12 UTC285
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC286
        ("x-shame", run.total_shame().to_string()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC287
        ("x-cash", run.cash.to_string()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC288
        ("x-floor", run.floor.number().to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC289
        ("link", links(run)),
a9b016e4b248atagen2026-10-10 08:12 UTC290
    ];
dfa38cf4cd4batagen2026-10-10 09:17 UTC291
    if let Some(observer) = run.observed {
dfa38cf4cd4batagen2026-10-10 09:17 UTC292
        stats.push(("x-observed-by", observer.into()));
a9b016e4b248atagen2026-10-10 08:12 UTC293
    }
a9b016e4b248atagen2026-10-10 08:12 UTC294
    if run.mom_present {
a9b016e4b248atagen2026-10-10 08:12 UTC295
        stats.push(("via", "1.1 mom".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC296
        stats.push(("vary", "Mom".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC297
    }
a9b016e4b248atagen2026-10-10 08:12 UTC298
    // RFC 9111 obsoleted Warning, which is why nobody heeds it.
a9b016e4b248atagen2026-10-10 08:12 UTC299
    if run.eq > 70 && run.ended.is_none() {
a9b016e4b248atagen2026-10-10 08:12 UTC300
        stats.push(("warning", "199 - \"it is turning purple\"".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC301
    }
a9b016e4b248atagen2026-10-10 08:12 UTC302
    stats.push((
a9b016e4b248atagen2026-10-10 08:12 UTC303
        "set-cookie",
a9b016e4b248atagen2026-10-10 08:12 UTC304
        format!("shame={}; {COOKIE_ATTRIBUTES}", run.total_shame()),
a9b016e4b248atagen2026-10-10 08:12 UTC305
    ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC306
    if run.items.ruler {
a9b016e4b248atagen2026-10-10 08:12 UTC307
        stats.push(("set-cookie", format!("ruler=1; {COOKIE_ATTRIBUTES}")));
a9b016e4b248atagen2026-10-10 08:12 UTC308
    }
a9b016e4b248atagen2026-10-10 08:12 UTC309
    let headers = response.headers_mut();
a9b016e4b248atagen2026-10-10 08:12 UTC310
    for (name, value) in stats {
a9b016e4b248atagen2026-10-10 08:12 UTC311
        headers.append(name, HeaderValue::from_str(&value).expect("visible ascii"));
a9b016e4b248atagen2026-10-10 08:12 UTC312
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC313
    let bodiless = status.is_informational() || status == StatusCode::NO_CONTENT;
dfa38cf4cd4batagen2026-10-10 09:17 UTC314
    if run.items.ring && !bodiless {
dfa38cf4cd4batagen2026-10-10 09:17 UTC315
        response.extensions_mut().insert(Compress);
dfa38cf4cd4batagen2026-10-10 09:17 UTC316
    }
a9b016e4b248atagen2026-10-10 08:12 UTC317
    response
a9b016e4b248atagen2026-10-10 08:12 UTC318
}
a9b016e4b248atagen2026-10-10 08:12 UTC319
a9b016e4b248atagen2026-10-10 08:12 UTC320
fn gone(run: &Run) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC321
    let ending = run.ended.as_ref().expect("only called on ended runs");
a9b016e4b248atagen2026-10-10 08:12 UTC322
    Problem::new(StatusCode::GONE, "run-ended", "This run has ended.")
a9b016e4b248atagen2026-10-10 08:12 UTC323
        .with("cause", format!("{PROBLEM_BASE}{}", ending.cause))
a9b016e4b248atagen2026-10-10 08:12 UTC324
        .with("turn", ending.turn)
a9b016e4b248atagen2026-10-10 08:12 UTC325
        .with("graveyard", "/graveyard")
a9b016e4b248atagen2026-10-10 08:12 UTC326
        .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC327
}
a9b016e4b248atagen2026-10-10 08:12 UTC328
dfa38cf4cd4batagen2026-10-10 09:17 UTC329
fn captive_portal(run: &Run) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC330
    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC331
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC332
        "captive-portal",
dfa38cf4cd4batagen2026-10-10 09:17 UTC333
        "The network requires authentication. The hotel would like you to agree to some terms.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC334
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC335
    .with("portal", format!("/runs/{}/portal", run.id))
dfa38cf4cd4batagen2026-10-10 09:17 UTC336
    .header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC337
        header::LINK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC338
        format!("</runs/{}/portal>; rel=\"{REL_BASE}portal\"", run.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC339
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC340
    .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC341
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC342
a9b016e4b248atagen2026-10-10 08:12 UTC343
fn with_run(
a9b016e4b248atagen2026-10-10 08:12 UTC344
    state: &AppState,
a9b016e4b248atagen2026-10-10 08:12 UTC345
    id: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC346
    headers: &HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC347
    scope: Scope,
a9b016e4b248atagen2026-10-10 08:12 UTC348
    handle: impl FnOnce(&mut Run) -> Response,
a9b016e4b248atagen2026-10-10 08:12 UTC349
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC350
    let mut runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC351
    let Some(run) = runs.get_mut(id) else {
a9b016e4b248atagen2026-10-10 08:12 UTC352
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC353
            StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC354
            "no-such-run",
a9b016e4b248atagen2026-10-10 08:12 UTC355
            format!("There is no run {id}. There may never have been."),
a9b016e4b248atagen2026-10-10 08:12 UTC356
        )
a9b016e4b248atagen2026-10-10 08:12 UTC357
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC358
    };
a9b016e4b248atagen2026-10-10 08:12 UTC359
    if run.ended.is_some() {
a9b016e4b248atagen2026-10-10 08:12 UTC360
        return gone(run);
a9b016e4b248atagen2026-10-10 08:12 UTC361
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC362
    run.observed = None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC363
    if scope == Scope::Home {
dfa38cf4cd4batagen2026-10-10 09:17 UTC364
        if run.floor == Floor::Hotel && !run.wifi_accepted {
dfa38cf4cd4batagen2026-10-10 09:17 UTC365
            return finish(run, captive_portal(run));
dfa38cf4cd4batagen2026-10-10 09:17 UTC366
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC367
        if run.floor.has_mom()
dfa38cf4cd4batagen2026-10-10 09:17 UTC368
            && run.mom_present
dfa38cf4cd4batagen2026-10-10 09:17 UTC369
            && let Err(problem) = run.answer_door(header_str(headers, header::AUTHORIZATION))
dfa38cf4cd4batagen2026-10-10 09:17 UTC370
        {
dfa38cf4cd4batagen2026-10-10 09:17 UTC371
            return finish(run, problem.into_response());
dfa38cf4cd4batagen2026-10-10 09:17 UTC372
        }
a9b016e4b248atagen2026-10-10 08:12 UTC373
    }
a9b016e4b248atagen2026-10-10 08:12 UTC374
    let response = handle(run);
a9b016e4b248atagen2026-10-10 08:12 UTC375
    finish(run, response)
a9b016e4b248atagen2026-10-10 08:12 UTC376
}
a9b016e4b248atagen2026-10-10 08:12 UTC377
a9b016e4b248atagen2026-10-10 08:12 UTC378
fn respond<T: IntoResponse, E: IntoResponse>(result: Result<T, E>) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC379
    match result {
a9b016e4b248atagen2026-10-10 08:12 UTC380
        Ok(ok) => ok.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC381
        Err(err) => err.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC382
    }
a9b016e4b248atagen2026-10-10 08:12 UTC383
}
a9b016e4b248atagen2026-10-10 08:12 UTC384
a9b016e4b248atagen2026-10-10 08:12 UTC385
async fn runs(
a9b016e4b248atagen2026-10-10 08:12 UTC386
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC387
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC388
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC389
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC390
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC391
    if method != Method::POST {
a9b016e4b248atagen2026-10-10 08:12 UTC392
        return unsupported(&method, "POST");
a9b016e4b248atagen2026-10-10 08:12 UTC393
    }
a9b016e4b248atagen2026-10-10 08:12 UTC394
    let seed = match query.get("seed").map(|s| s.parse::<u64>()) {
a9b016e4b248atagen2026-10-10 08:12 UTC395
        None => rand::rng().random(),
a9b016e4b248atagen2026-10-10 08:12 UTC396
        Some(Ok(seed)) => seed,
a9b016e4b248atagen2026-10-10 08:12 UTC397
        Some(Err(_)) => {
a9b016e4b248atagen2026-10-10 08:12 UTC398
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC399
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC400
                "bad-seed",
a9b016e4b248atagen2026-10-10 08:12 UTC401
                "The seed must be an unsigned 64-bit integer.",
a9b016e4b248atagen2026-10-10 08:12 UTC402
            )
a9b016e4b248atagen2026-10-10 08:12 UTC403
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC404
        }
a9b016e4b248atagen2026-10-10 08:12 UTC405
    };
a9b016e4b248atagen2026-10-10 08:12 UTC406
    let shame = cookie(&headers, "shame")
a9b016e4b248atagen2026-10-10 08:12 UTC407
        .and_then(|s| s.parse().ok())
a9b016e4b248atagen2026-10-10 08:12 UTC408
        .unwrap_or(0);
a9b016e4b248atagen2026-10-10 08:12 UTC409
    let id = format!("{:08x}", rand::rng().random::<u32>());
a9b016e4b248atagen2026-10-10 08:12 UTC410
    let run = Run::new(id.clone(), seed, shame);
a9b016e4b248atagen2026-10-10 08:12 UTC411
    let response = (
a9b016e4b248atagen2026-10-10 08:12 UTC412
        StatusCode::CREATED,
a9b016e4b248atagen2026-10-10 08:12 UTC413
        [
a9b016e4b248atagen2026-10-10 08:12 UTC414
            (header::LOCATION, format!("/runs/{id}")),
a9b016e4b248atagen2026-10-10 08:12 UTC415
            (header::ETAG, run.etag()),
a9b016e4b248atagen2026-10-10 08:12 UTC416
            (HeaderName::from_static("x-seed"), seed.to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC417
        ],
a9b016e4b248atagen2026-10-10 08:12 UTC418
        Json(run.view()),
a9b016e4b248atagen2026-10-10 08:12 UTC419
    )
a9b016e4b248atagen2026-10-10 08:12 UTC420
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC421
    let response = finish(&run, response);
a9b016e4b248atagen2026-10-10 08:12 UTC422
    state.insert(run);
a9b016e4b248atagen2026-10-10 08:12 UTC423
    response
a9b016e4b248atagen2026-10-10 08:12 UTC424
}
a9b016e4b248atagen2026-10-10 08:12 UTC425
a9b016e4b248atagen2026-10-10 08:12 UTC426
#[allow(clippy::too_many_arguments)]
a9b016e4b248atagen2026-10-10 08:12 UTC427
async fn run(
a9b016e4b248atagen2026-10-10 08:12 UTC428
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC429
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC430
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC431
    uri: Uri,
a9b016e4b248atagen2026-10-10 08:12 UTC432
    version: Version,
a9b016e4b248atagen2026-10-10 08:12 UTC433
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC434
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC435
    body: Bytes,
a9b016e4b248atagen2026-10-10 08:12 UTC436
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC437
    let now = Instant::now();
dfa38cf4cd4batagen2026-10-10 09:17 UTC438
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC439
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC440
            "GET" | "HEAD" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC441
                let accept = header_str(&headers, header::ACCEPT).unwrap_or_default();
dfa38cf4cd4batagen2026-10-10 09:17 UTC442
                if accept.contains("reassurance") {
dfa38cf4cd4batagen2026-10-10 09:17 UTC443
                    return Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC444
                        StatusCode::NOT_ACCEPTABLE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC445
                        "no-reassurance",
dfa38cf4cd4batagen2026-10-10 09:17 UTC446
                        "The server does not produce reassurance.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC447
                    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC448
                    .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC449
                }
dfa38cf4cd4batagen2026-10-10 09:17 UTC450
                (
dfa38cf4cd4batagen2026-10-10 09:17 UTC451
                    StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC452
                    [(header::ETAG, run.etag())],
dfa38cf4cd4batagen2026-10-10 09:17 UTC453
                    Json(run.view()),
a9b016e4b248atagen2026-10-10 08:12 UTC454
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC455
                    .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC456
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC457
            "JELQ" => jelq(run, &query, &headers, now),
dfa38cf4cd4batagen2026-10-10 09:17 UTC458
            "PUT" => run.put().into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
            "PATCH" => patch(run, &headers, &body),
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
            "DELETE" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC461
                run.end("abandoned", "The run was abandoned.");
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
                StatusCode::NO_CONTENT.into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
            "OPTIONS" => options(RUN_ALLOW, true),
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
            "TRACE" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC466
                run.shame += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC467
                trace(&method, &uri, version, &headers)
dfa38cf4cd4batagen2026-10-10 09:17 UTC468
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC469
            _ => unsupported(&method, RUN_ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC470
        }
a9b016e4b248atagen2026-10-10 08:12 UTC471
    })
a9b016e4b248atagen2026-10-10 08:12 UTC472
}
a9b016e4b248atagen2026-10-10 08:12 UTC473
a9b016e4b248atagen2026-10-10 08:12 UTC474
fn jelq(
a9b016e4b248atagen2026-10-10 08:12 UTC475
    run: &mut Run,
a9b016e4b248atagen2026-10-10 08:12 UTC476
    query: &HashMap<String, String>,
a9b016e4b248atagen2026-10-10 08:12 UTC477
    headers: &HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC478
    now: Instant,
a9b016e4b248atagen2026-10-10 08:12 UTC479
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC480
    let techniques_href = format!("/runs/{}/techniques", run.id);
dfa38cf4cd4batagen2026-10-10 09:17 UTC481
    let Some(names) = query.get("technique") else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC482
        return Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC483
            StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC484
            "technique-required",
dfa38cf4cd4batagen2026-10-10 09:17 UTC485
            "No technique was specified.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC486
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC487
        .with("techniques", techniques_href)
dfa38cf4cd4batagen2026-10-10 09:17 UTC488
        .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC489
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC490
    let mut techniques = Vec::new();
dfa38cf4cd4batagen2026-10-10 09:17 UTC491
    for name in names.split(',').map(str::trim) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC492
        let Some(technique) = Technique::parse(name) else {
a9b016e4b248atagen2026-10-10 08:12 UTC493
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC494
                StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC495
                "unknown-technique",
dfa38cf4cd4batagen2026-10-10 09:17 UTC496
                format!("{name} is not a technique."),
a9b016e4b248atagen2026-10-10 08:12 UTC497
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC498
            .with("techniques", techniques_href)
a9b016e4b248atagen2026-10-10 08:12 UTC499
            .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
        techniques.push(technique);
dfa38cf4cd4batagen2026-10-10 09:17 UTC502
    }
a9b016e4b248atagen2026-10-10 08:12 UTC503
    let intensity = match query.get("intensity").map(|i| i.parse::<i32>()) {
a9b016e4b248atagen2026-10-10 08:12 UTC504
        None => 5,
a9b016e4b248atagen2026-10-10 08:12 UTC505
        Some(Ok(i)) if (1..=10).contains(&i) => i,
a9b016e4b248atagen2026-10-10 08:12 UTC506
        Some(_) => {
a9b016e4b248atagen2026-10-10 08:12 UTC507
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC508
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC509
                "intensity-out-of-range",
a9b016e4b248atagen2026-10-10 08:12 UTC510
                "Intensity is measured from 1 to 10.",
a9b016e4b248atagen2026-10-10 08:12 UTC511
            )
a9b016e4b248atagen2026-10-10 08:12 UTC512
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC513
        }
a9b016e4b248atagen2026-10-10 08:12 UTC514
    };
a9b016e4b248atagen2026-10-10 08:12 UTC515
    let lube = match Lube::from_content_type(header_str(headers, header::CONTENT_TYPE)) {
a9b016e4b248atagen2026-10-10 08:12 UTC516
        Ok(lube) => lube,
a9b016e4b248atagen2026-10-10 08:12 UTC517
        Err(problem) => return problem.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC518
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC519
    respond(run.jelq(&techniques, intensity, lube, now))
a9b016e4b248atagen2026-10-10 08:12 UTC520
}
a9b016e4b248atagen2026-10-10 08:12 UTC521
a9b016e4b248atagen2026-10-10 08:12 UTC522
fn patch(run: &mut Run, headers: &HeaderMap, body: &[u8]) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC523
    let content_type = header_str(headers, header::CONTENT_TYPE).unwrap_or_default();
a9b016e4b248atagen2026-10-10 08:12 UTC524
    if !content_type.starts_with("application/json-patch+json") {
a9b016e4b248atagen2026-10-10 08:12 UTC525
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC526
            StatusCode::UNSUPPORTED_MEDIA_TYPE,
a9b016e4b248atagen2026-10-10 08:12 UTC527
            "patch-format",
a9b016e4b248atagen2026-10-10 08:12 UTC528
            "Patches are accepted as application/json-patch+json.",
a9b016e4b248atagen2026-10-10 08:12 UTC529
        )
a9b016e4b248atagen2026-10-10 08:12 UTC530
        .header(
a9b016e4b248atagen2026-10-10 08:12 UTC531
            HeaderName::from_static("accept-patch"),
a9b016e4b248atagen2026-10-10 08:12 UTC532
            "application/json-patch+json",
a9b016e4b248atagen2026-10-10 08:12 UTC533
        )
a9b016e4b248atagen2026-10-10 08:12 UTC534
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC535
    }
a9b016e4b248atagen2026-10-10 08:12 UTC536
    match serde_json::from_slice::<Vec<PatchOp>>(body) {
a9b016e4b248atagen2026-10-10 08:12 UTC537
        Ok(ops) => respond(run.patch(&ops)),
a9b016e4b248atagen2026-10-10 08:12 UTC538
        Err(_) => Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC539
            StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC540
            "malformed-patch",
a9b016e4b248atagen2026-10-10 08:12 UTC541
            "The patch could not be parsed.",
a9b016e4b248atagen2026-10-10 08:12 UTC542
        )
a9b016e4b248atagen2026-10-10 08:12 UTC543
        .into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC544
    }
a9b016e4b248atagen2026-10-10 08:12 UTC545
}
a9b016e4b248atagen2026-10-10 08:12 UTC546
dfa38cf4cd4batagen2026-10-10 09:17 UTC547
fn millimetres(inches: f64) -> usize {
dfa38cf4cd4batagen2026-10-10 09:17 UTC548
    (inches * MM_PER_INCH).round() as usize
dfa38cf4cd4batagen2026-10-10 09:17 UTC549
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC550
dfa38cf4cd4batagen2026-10-10 09:17 UTC551
fn parse_inch_range(range: &str, total: f64) -> Option<(f64, f64)> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC552
    let (start, end) = range.strip_prefix("inches=")?.split_once('-')?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC553
    let start: f64 = start.trim().parse().ok()?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC554
    let end: f64 = match end.trim() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC555
        "" => total,
dfa38cf4cd4batagen2026-10-10 09:17 UTC556
        end => end.parse().ok()?,
dfa38cf4cd4batagen2026-10-10 09:17 UTC557
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC558
    (start >= 0.0 && start < total && start <= end).then_some((start, end.min(total)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC559
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC560
dfa38cf4cd4batagen2026-10-10 09:17 UTC561
fn length_response(run: &Run, headers: &HeaderMap) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC562
    let inches = if run.items.ruler {
dfa38cf4cd4batagen2026-10-10 09:17 UTC563
        run.current_length()
dfa38cf4cd4batagen2026-10-10 09:17 UTC564
    } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC565
        run.self_reported()
dfa38cf4cd4batagen2026-10-10 09:17 UTC566
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC567
    let body = shaft(millimetres(inches));
dfa38cf4cd4batagen2026-10-10 09:17 UTC568
    let content_type = (
dfa38cf4cd4batagen2026-10-10 09:17 UTC569
        header::CONTENT_TYPE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC570
        "text/plain; charset=us-ascii".to_owned(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC571
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC572
    let accept_ranges = (header::ACCEPT_RANGES, "inches".to_owned());
dfa38cf4cd4batagen2026-10-10 09:17 UTC573
dfa38cf4cd4batagen2026-10-10 09:17 UTC574
    if let Some(range) = header_str(headers, header::RANGE)
dfa38cf4cd4batagen2026-10-10 09:17 UTC575
        && range.starts_with("inches=")
dfa38cf4cd4batagen2026-10-10 09:17 UTC576
    {
dfa38cf4cd4batagen2026-10-10 09:17 UTC577
        let Some((start, end)) = parse_inch_range(range, inches) else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC578
            return Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC579
                StatusCode::RANGE_NOT_SATISFIABLE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC580
                "range-not-satisfiable",
dfa38cf4cd4batagen2026-10-10 09:17 UTC581
                format!("The requested range is not available. {inches:.2} inches are."),
dfa38cf4cd4batagen2026-10-10 09:17 UTC582
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC583
            .header(header::CONTENT_RANGE, format!("inches */{inches:.2}"))
dfa38cf4cd4batagen2026-10-10 09:17 UTC584
            .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC585
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC586
        let from = millimetres(start).min(body.len() - 1);
dfa38cf4cd4batagen2026-10-10 09:17 UTC587
        let to = millimetres(end).clamp(from + 1, body.len());
dfa38cf4cd4batagen2026-10-10 09:17 UTC588
        let content_range = (
dfa38cf4cd4batagen2026-10-10 09:17 UTC589
            header::CONTENT_RANGE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC590
            format!("inches {start:.2}-{end:.2}/{inches:.2}"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC591
        );
dfa38cf4cd4batagen2026-10-10 09:17 UTC592
        return (
dfa38cf4cd4batagen2026-10-10 09:17 UTC593
            StatusCode::PARTIAL_CONTENT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC594
            [content_type, accept_ranges, content_range],
dfa38cf4cd4batagen2026-10-10 09:17 UTC595
            body[from..to].to_owned(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC596
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC597
            .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC598
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC599
dfa38cf4cd4batagen2026-10-10 09:17 UTC600
    if run.items.ruler {
dfa38cf4cd4batagen2026-10-10 09:17 UTC601
        return (StatusCode::OK, [content_type, accept_ranges], body).into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC602
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC603
    let chunks: Vec<Result<Bytes, Infallible>> = body
dfa38cf4cd4batagen2026-10-10 09:17 UTC604
        .into_bytes()
dfa38cf4cd4batagen2026-10-10 09:17 UTC605
        .chunks(16)
dfa38cf4cd4batagen2026-10-10 09:17 UTC606
        .map(|c| Ok(Bytes::copy_from_slice(c)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC607
        .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC608
    let body = Body::from_stream(futures_util::stream::iter(chunks));
dfa38cf4cd4batagen2026-10-10 09:17 UTC609
    (StatusCode::OK, [content_type, accept_ranges], body).into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC610
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC611
a9b016e4b248atagen2026-10-10 08:12 UTC612
async fn length(
a9b016e4b248atagen2026-10-10 08:12 UTC613
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC614
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC615
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC616
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC617
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC618
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC619
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC620
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC621
            "GET" | "HEAD" => length_response(run, &headers),
dfa38cf4cd4batagen2026-10-10 09:17 UTC622
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC623
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC624
        }
a9b016e4b248atagen2026-10-10 08:12 UTC625
    })
a9b016e4b248atagen2026-10-10 08:12 UTC626
}
a9b016e4b248atagen2026-10-10 08:12 UTC627
a9b016e4b248atagen2026-10-10 08:12 UTC628
async fn foreskin(
a9b016e4b248atagen2026-10-10 08:12 UTC629
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC630
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC631
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC632
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC633
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC634
    const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC635
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC636
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC637
            "GET" | "HEAD" => match run.foreskin {
dfa38cf4cd4batagen2026-10-10 09:17 UTC638
                Some(f) => (
dfa38cf4cd4batagen2026-10-10 09:17 UTC639
                    StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC640
                    [(header::ETAG, run.etag())],
dfa38cf4cd4batagen2026-10-10 09:17 UTC641
                    Json(json!({ "foreskin": (f * 100.0).round() / 100.0 })),
dfa38cf4cd4batagen2026-10-10 09:17 UTC642
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC643
                    .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC644
                None => Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC645
                    StatusCode::GONE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC646
                    "foreskin-gone",
dfa38cf4cd4batagen2026-10-10 09:17 UTC647
                    "The foreskin is gone. It will not be returning.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC648
                )
a9b016e4b248atagen2026-10-10 08:12 UTC649
                .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC650
            },
dfa38cf4cd4batagen2026-10-10 09:17 UTC651
            "DELETE" => respond(run.delete_foreskin(header_str(&headers, header::IF_MATCH))),
dfa38cf4cd4batagen2026-10-10 09:17 UTC652
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC653
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC654
        }
a9b016e4b248atagen2026-10-10 08:12 UTC655
    })
a9b016e4b248atagen2026-10-10 08:12 UTC656
}
a9b016e4b248atagen2026-10-10 08:12 UTC657
a9b016e4b248atagen2026-10-10 08:12 UTC658
async fn inventory(
a9b016e4b248atagen2026-10-10 08:12 UTC659
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC660
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC661
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC662
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC663
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC664
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC665
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC666
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC667
            "GET" | "HEAD" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC668
                let listed: Vec<Value> = run
dfa38cf4cd4batagen2026-10-10 09:17 UTC669
                    .inventory()
dfa38cf4cd4batagen2026-10-10 09:17 UTC670
                    .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC671
                    .map(|(name, _)| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC672
                        json!({ "name": name, "href": format!("/runs/{}/inventory/{name}", run.id) })
dfa38cf4cd4batagen2026-10-10 09:17 UTC673
                    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC674
                    .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC675
                Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC676
                    "items": listed,
dfa38cf4cd4batagen2026-10-10 09:17 UTC677
                    "ice_packs": run.items.ice_packs,
dfa38cf4cd4batagen2026-10-10 09:17 UTC678
                    "cash": run.cash,
dfa38cf4cd4batagen2026-10-10 09:17 UTC679
                    "excuses": run.excuses(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC680
                }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC681
                .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC682
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC683
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC684
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC685
        }
a9b016e4b248atagen2026-10-10 08:12 UTC686
    })
a9b016e4b248atagen2026-10-10 08:12 UTC687
}
a9b016e4b248atagen2026-10-10 08:12 UTC688
a9b016e4b248atagen2026-10-10 08:12 UTC689
async fn item(
a9b016e4b248atagen2026-10-10 08:12 UTC690
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC691
    Path((id, name)): Path<(String, String)>,
a9b016e4b248atagen2026-10-10 08:12 UTC692
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC693
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC694
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC695
    const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC696
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC697
        let Some((name, description)) = run.inventory().into_iter().find(|(n, _)| *n == name)
dfa38cf4cd4batagen2026-10-10 09:17 UTC698
        else {
a9b016e4b248atagen2026-10-10 08:12 UTC699
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC700
                StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC701
                "no-such-item",
a9b016e4b248atagen2026-10-10 08:12 UTC702
                "You do not have that.",
a9b016e4b248atagen2026-10-10 08:12 UTC703
            )
a9b016e4b248atagen2026-10-10 08:12 UTC704
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC705
        };
a9b016e4b248atagen2026-10-10 08:12 UTC706
        match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC707
            "GET" | "HEAD" => {
a9b016e4b248atagen2026-10-10 08:12 UTC708
                Json(json!({ "name": name, "description": description })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC709
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC710
            "DELETE" => respond(run.drop_item(name)),
a9b016e4b248atagen2026-10-10 08:12 UTC711
            "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 UTC712
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC713
        }
a9b016e4b248atagen2026-10-10 08:12 UTC714
    })
a9b016e4b248atagen2026-10-10 08:12 UTC715
}
a9b016e4b248atagen2026-10-10 08:12 UTC716
a9b016e4b248atagen2026-10-10 08:12 UTC717
async fn techniques(
a9b016e4b248atagen2026-10-10 08:12 UTC718
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC719
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC720
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC721
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC722
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC723
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC724
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC725
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC726
            "GET" | "HEAD" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC727
                let listed: Vec<Value> = Technique::ALL
dfa38cf4cd4batagen2026-10-10 09:17 UTC728
                    .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC729
                    .map(|t| t.describe(run.forum_read))
dfa38cf4cd4batagen2026-10-10 09:17 UTC730
                    .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC731
                Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC732
                    "usage": format!("JELQ /runs/{}?technique=<name>[,<name>]&intensity=<1-10>", run.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC733
                    "lubricant": "Declared with Content-Type.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC734
                    "lubricants": LUBRICANTS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC735
                    "eq_band": [50, 70],
dfa38cf4cd4batagen2026-10-10 09:17 UTC736
                    "techniques": listed,
dfa38cf4cd4batagen2026-10-10 09:17 UTC737
                }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC738
                .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC739
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC740
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC741
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC742
        }
a9b016e4b248atagen2026-10-10 08:12 UTC743
    })
a9b016e4b248atagen2026-10-10 08:12 UTC744
}
a9b016e4b248atagen2026-10-10 08:12 UTC745
a9b016e4b248atagen2026-10-10 08:12 UTC746
async fn drawer(
a9b016e4b248atagen2026-10-10 08:12 UTC747
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC748
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC749
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC750
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC751
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC752
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC753
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC754
            "POST" => respond(run.search_drawer()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC755
            "OPTIONS" => options("POST, OPTIONS", false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC756
            _ => unsupported(&method, "POST, OPTIONS"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC757
        }
a9b016e4b248atagen2026-10-10 08:12 UTC758
    })
a9b016e4b248atagen2026-10-10 08:12 UTC759
}
a9b016e4b248atagen2026-10-10 08:12 UTC760
a9b016e4b248atagen2026-10-10 08:12 UTC761
async fn branch(
a9b016e4b248atagen2026-10-10 08:12 UTC762
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC763
    Path((id, choice)): Path<(String, String)>,
a9b016e4b248atagen2026-10-10 08:12 UTC764
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC765
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC766
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC767
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC768
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC769
            "POST" => respond(run.choose(&choice)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC770
            "OPTIONS" => options("POST, OPTIONS", false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC771
            _ => unsupported(&method, "POST, OPTIONS"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC772
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC773
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC774
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC775
dfa38cf4cd4batagen2026-10-10 09:17 UTC776
async fn floor(
dfa38cf4cd4batagen2026-10-10 09:17 UTC777
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC778
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC779
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC780
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC781
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC782
    const ALLOW: &str = "GET, HEAD, POST, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC783
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC784
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC785
            "GET" | "HEAD" => Json(run.floor_view()).into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC786
            "POST" => respond(run.advance()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC787
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC788
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC789
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC790
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC791
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC792
dfa38cf4cd4batagen2026-10-10 09:17 UTC793
async fn shop(
dfa38cf4cd4batagen2026-10-10 09:17 UTC794
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC795
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC796
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC797
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC798
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC799
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC800
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC801
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC802
            "GET" | "HEAD" => respond(run.shop_view().map(Json)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC803
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC804
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC805
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC806
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC807
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC808
dfa38cf4cd4batagen2026-10-10 09:17 UTC809
async fn buy(
dfa38cf4cd4batagen2026-10-10 09:17 UTC810
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC811
    Path((id, item)): Path<(String, String)>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC812
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC813
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC814
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC815
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC816
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC817
            "POST" => respond(run.buy(&item)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC818
            "OPTIONS" => options("POST, OPTIONS", false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC819
            _ => unsupported(&method, "POST, OPTIONS"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC820
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC821
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC822
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC823
dfa38cf4cd4batagen2026-10-10 09:17 UTC824
async fn portal(
dfa38cf4cd4batagen2026-10-10 09:17 UTC825
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC826
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC827
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC828
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC829
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC830
    const ALLOW: &str = "GET, HEAD, POST, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC831
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC832
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC833
            "GET" | "HEAD" => Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC834
                "terms": "By connecting you agree that the hotel is not responsible for anything that happens in this room, and that it does not want to know.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC835
                "accept": { "href": format!("/runs/{}/portal", run.id), "method": "POST" },
dfa38cf4cd4batagen2026-10-10 09:17 UTC836
            }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC837
            .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC838
            "POST" => respond(run.accept_terms()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC839
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC840
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC841
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC842
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC843
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC844
dfa38cf4cd4batagen2026-10-10 09:17 UTC845
async fn elder(
dfa38cf4cd4batagen2026-10-10 09:17 UTC846
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC847
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC848
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC849
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC850
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC851
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC852
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC853
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC854
            "GET" | "HEAD" => respond(run.elder(
dfa38cf4cd4batagen2026-10-10 09:17 UTC855
                header_str(&headers, header::PROXY_AUTHORIZATION),
dfa38cf4cd4batagen2026-10-10 09:17 UTC856
                header_str(&headers, header::IF_MODIFIED_SINCE),
dfa38cf4cd4batagen2026-10-10 09:17 UTC857
            )),
dfa38cf4cd4batagen2026-10-10 09:17 UTC858
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC859
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC860
        }
a9b016e4b248atagen2026-10-10 08:12 UTC861
    })
a9b016e4b248atagen2026-10-10 08:12 UTC862
}
a9b016e4b248atagen2026-10-10 08:12 UTC863
dfa38cf4cd4batagen2026-10-10 09:17 UTC864
fn requested_grip(headers: &HeaderMap) -> Option<Grip> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC865
    let connection = header_str(headers, header::CONNECTION)?.to_ascii_lowercase();
dfa38cf4cd4batagen2026-10-10 09:17 UTC866
    if !connection.split(',').any(|token| token.trim() == "upgrade") {
dfa38cf4cd4batagen2026-10-10 09:17 UTC867
        return None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC868
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC869
    header_str(headers, header::UPGRADE)?
dfa38cf4cd4batagen2026-10-10 09:17 UTC870
        .split(',')
dfa38cf4cd4batagen2026-10-10 09:17 UTC871
        .find_map(|protocol| Grip::parse(protocol.trim()))
dfa38cf4cd4batagen2026-10-10 09:17 UTC872
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC873
dfa38cf4cd4batagen2026-10-10 09:17 UTC874
async fn grip(
dfa38cf4cd4batagen2026-10-10 09:17 UTC875
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC876
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC877
    mut request: Request,
dfa38cf4cd4batagen2026-10-10 09:17 UTC878
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC879
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC880
    let method = request.method().clone();
dfa38cf4cd4batagen2026-10-10 09:17 UTC881
    let headers = request.headers().clone();
dfa38cf4cd4batagen2026-10-10 09:17 UTC882
    let wanted = requested_grip(&headers);
dfa38cf4cd4batagen2026-10-10 09:17 UTC883
    let upgrade = wanted.map(|_| hyper::upgrade::on(&mut request));
dfa38cf4cd4batagen2026-10-10 09:17 UTC884
    let response = with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC885
        match (method.as_str(), wanted) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC886
            ("GET" | "HEAD", Some(grip)) => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC887
                run.set_grip(grip);
dfa38cf4cd4batagen2026-10-10 09:17 UTC888
                (
dfa38cf4cd4batagen2026-10-10 09:17 UTC889
                    StatusCode::SWITCHING_PROTOCOLS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC890
                    [
dfa38cf4cd4batagen2026-10-10 09:17 UTC891
                        (header::UPGRADE, grip.name()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC892
                        (header::CONNECTION, "upgrade"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC893
                    ],
dfa38cf4cd4batagen2026-10-10 09:17 UTC894
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC895
                    .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC896
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC897
            ("GET" | "HEAD", None) => (
dfa38cf4cd4batagen2026-10-10 09:17 UTC898
                StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC899
                [(header::UPGRADE, "overhand, underhand")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC900
                Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC901
                    "grip": run.grip.name(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC902
                    "detail": "Grips are protocols. Change grip by upgrading the connection.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC903
                })),
dfa38cf4cd4batagen2026-10-10 09:17 UTC904
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC905
                .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC906
            ("OPTIONS", _) => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC907
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC908
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC909
    });
dfa38cf4cd4batagen2026-10-10 09:17 UTC910
    if response.status() == StatusCode::SWITCHING_PROTOCOLS
dfa38cf4cd4batagen2026-10-10 09:17 UTC911
        && let (Some(upgrade), Some(grip)) = (upgrade, wanted)
dfa38cf4cd4batagen2026-10-10 09:17 UTC912
    {
dfa38cf4cd4batagen2026-10-10 09:17 UTC913
        tokio::spawn(async move {
dfa38cf4cd4batagen2026-10-10 09:17 UTC914
            if let Ok(upgraded) = upgrade.await {
dfa38cf4cd4batagen2026-10-10 09:17 UTC915
                let mut io = TokioIo::new(upgraded);
dfa38cf4cd4batagen2026-10-10 09:17 UTC916
                let line = format!("You are now jelqing {}.\r\n", grip.name());
dfa38cf4cd4batagen2026-10-10 09:17 UTC917
                let _ = io.write_all(line.as_bytes()).await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC918
                let _ = io.shutdown().await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC919
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC920
        });
dfa38cf4cd4batagen2026-10-10 09:17 UTC921
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC922
    response
dfa38cf4cd4batagen2026-10-10 09:17 UTC923
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC924
a9b016e4b248atagen2026-10-10 08:12 UTC925
async fn diagnosis(
a9b016e4b248atagen2026-10-10 08:12 UTC926
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC927
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC928
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC929
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC930
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC931
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC932
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC933
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC934
            "GET" | "HEAD" => respond(run.diagnose(
dfa38cf4cd4batagen2026-10-10 09:17 UTC935
                header_str(&headers, header::AUTHORIZATION),
dfa38cf4cd4batagen2026-10-10 09:17 UTC936
                header_str(&headers, header::IF_NONE_MATCH),
dfa38cf4cd4batagen2026-10-10 09:17 UTC937
            )),
dfa38cf4cd4batagen2026-10-10 09:17 UTC938
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC939
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC940
        }
a9b016e4b248atagen2026-10-10 08:12 UTC941
    })
a9b016e4b248atagen2026-10-10 08:12 UTC942
}
a9b016e4b248atagen2026-10-10 08:12 UTC943
dfa38cf4cd4batagen2026-10-10 09:17 UTC944
async fn thread(Path(year): Path<String>, method: Method) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC945
    if method != Method::GET && method != Method::HEAD {
dfa38cf4cd4batagen2026-10-10 09:17 UTC946
        return unsupported(&method, "GET, HEAD");
dfa38cf4cd4batagen2026-10-10 09:17 UTC947
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC948
    match year.parse::<u32>() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC949
        Ok(2009) => (
dfa38cf4cd4batagen2026-10-10 09:17 UTC950
            [(header::LAST_MODIFIED, ELDER_LAST_MODIFIED)],
dfa38cf4cd4batagen2026-10-10 09:17 UTC951
            Json(forum_thread()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC952
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC953
            .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC954
        Ok(y) if y > 2009 => Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC955
            StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC956
            "no-thread",
dfa38cf4cd4batagen2026-10-10 09:17 UTC957
            "Nobody has posted since 2009.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC958
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC959
        .with("thread", FORUM_THREAD)
dfa38cf4cd4batagen2026-10-10 09:17 UTC960
        .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC961
        _ => Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC962
            StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC963
            "no-thread",
dfa38cf4cd4batagen2026-10-10 09:17 UTC964
            "The forum did not exist yet. The problem did.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC965
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC966
        .with("thread", FORUM_THREAD)
dfa38cf4cd4batagen2026-10-10 09:17 UTC967
        .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC968
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC969
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC970
a9b016e4b248atagen2026-10-10 08:12 UTC971
async fn graveyard(State(state): State<Shared>, method: Method) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC972
    if method != Method::GET && method != Method::HEAD {
a9b016e4b248atagen2026-10-10 08:12 UTC973
        return unsupported(&method, "GET, HEAD");
a9b016e4b248atagen2026-10-10 08:12 UTC974
    }
a9b016e4b248atagen2026-10-10 08:12 UTC975
    let runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC976
    let dead: Vec<Value> = runs
a9b016e4b248atagen2026-10-10 08:12 UTC977
        .values()
a9b016e4b248atagen2026-10-10 08:12 UTC978
        .filter_map(|run| {
a9b016e4b248atagen2026-10-10 08:12 UTC979
            let ending = run.ended.as_ref()?;
a9b016e4b248atagen2026-10-10 08:12 UTC980
            Some(json!({
a9b016e4b248atagen2026-10-10 08:12 UTC981
                "id": run.id,
a9b016e4b248atagen2026-10-10 08:12 UTC982
                "cause": format!("{PROBLEM_BASE}{}", ending.cause),
a9b016e4b248atagen2026-10-10 08:12 UTC983
                "detail": ending.detail,
a9b016e4b248atagen2026-10-10 08:12 UTC984
                "turn": ending.turn,
a9b016e4b248atagen2026-10-10 08:12 UTC985
            }))
a9b016e4b248atagen2026-10-10 08:12 UTC986
        })
a9b016e4b248atagen2026-10-10 08:12 UTC987
        .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC988
    Json(json!({ "runs": dead })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC989
}
a9b016e4b248atagen2026-10-10 08:12 UTC990
a9b016e4b248atagen2026-10-10 08:12 UTC991
async fn leaderboard(
a9b016e4b248atagen2026-10-10 08:12 UTC992
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC993
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC994
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC995
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC996
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC997
    if method != Method::GET && method != Method::HEAD {
a9b016e4b248atagen2026-10-10 08:12 UTC998
        return unsupported(&method, "GET, HEAD");
a9b016e4b248atagen2026-10-10 08:12 UTC999
    }
a9b016e4b248atagen2026-10-10 08:12 UTC1000
    let truthful = match query.get("sort").map(String::as_str) {
a9b016e4b248atagen2026-10-10 08:12 UTC1001
        None | Some("self-reported-length") => false,
a9b016e4b248atagen2026-10-10 08:12 UTC1002
        Some("content-length") if cookie(&headers, "ruler") == Some("1") => true,
a9b016e4b248atagen2026-10-10 08:12 UTC1003
        Some("content-length") => {
a9b016e4b248atagen2026-10-10 08:12 UTC1004
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC1005
                StatusCode::FORBIDDEN,
a9b016e4b248atagen2026-10-10 08:12 UTC1006
                "ruler-required",
a9b016e4b248atagen2026-10-10 08:12 UTC1007
                "Sorting by the truth requires a Ruler.",
a9b016e4b248atagen2026-10-10 08:12 UTC1008
            )
a9b016e4b248atagen2026-10-10 08:12 UTC1009
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC1010
        }
a9b016e4b248atagen2026-10-10 08:12 UTC1011
        Some(_) => {
a9b016e4b248atagen2026-10-10 08:12 UTC1012
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC1013
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC1014
                "unknown-sort",
a9b016e4b248atagen2026-10-10 08:12 UTC1015
                "The leaderboard sorts by self-reported-length or content-length.",
a9b016e4b248atagen2026-10-10 08:12 UTC1016
            )
a9b016e4b248atagen2026-10-10 08:12 UTC1017
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC1018
        }
a9b016e4b248atagen2026-10-10 08:12 UTC1019
    };
a9b016e4b248atagen2026-10-10 08:12 UTC1020
    let runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC1021
    let mut entries: Vec<&Run> = runs.values().collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC1022
    let key = |r: &Run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1023
        if truthful {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1024
            r.current_length()
dfa38cf4cd4batagen2026-10-10 09:17 UTC1025
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1026
            r.self_reported()
dfa38cf4cd4batagen2026-10-10 09:17 UTC1027
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC1028
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC1029
    entries.sort_by(|a, b| key(b).total_cmp(&key(a)));
a9b016e4b248atagen2026-10-10 08:12 UTC1030
    let entries: Vec<Value> = entries
a9b016e4b248atagen2026-10-10 08:12 UTC1031
        .into_iter()
a9b016e4b248atagen2026-10-10 08:12 UTC1032
        .map(|run| {
a9b016e4b248atagen2026-10-10 08:12 UTC1033
            let mut entry = json!({
a9b016e4b248atagen2026-10-10 08:12 UTC1034
                "id": run.id,
a9b016e4b248atagen2026-10-10 08:12 UTC1035
                "status": run.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC1036
                "floor": run.floor.number(),
a9b016e4b248atagen2026-10-10 08:12 UTC1037
                "self_reported_length": (run.self_reported() * 100.0).round() / 100.0,
a9b016e4b248atagen2026-10-10 08:12 UTC1038
            });
a9b016e4b248atagen2026-10-10 08:12 UTC1039
            if truthful {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1040
                entry["content_length"] = json!(millimetres(run.current_length()));
a9b016e4b248atagen2026-10-10 08:12 UTC1041
            }
a9b016e4b248atagen2026-10-10 08:12 UTC1042
            entry
a9b016e4b248atagen2026-10-10 08:12 UTC1043
        })
a9b016e4b248atagen2026-10-10 08:12 UTC1044
        .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC1045
    Json(json!({ "entries": entries })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC1046
}
a9b016e4b248atagen2026-10-10 08:12 UTC1047
a9b016e4b248atagen2026-10-10 08:12 UTC1048
async fn not_found() -> Problem {
a9b016e4b248atagen2026-10-10 08:12 UTC1049
    Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC1050
        StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC1051
        "not-found",
a9b016e4b248atagen2026-10-10 08:12 UTC1052
        "The resource does not exist. This is not about you.",
a9b016e4b248atagen2026-10-10 08:12 UTC1053
    )
a9b016e4b248atagen2026-10-10 08:12 UTC1054
}