atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
177535ac8abf29d1d6eaa1bd35a72ae370329b5f
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
    );
177535ac8abfatagen2026-10-10 10:39 UTC193
    if method.as_str() == "BREW" {
177535ac8abfatagen2026-10-10 10:39 UTC194
        return Problem::new(
177535ac8abfatagen2026-10-10 10:39 UTC195
            StatusCode::IM_A_TEAPOT,
177535ac8abfatagen2026-10-10 10:39 UTC196
            "brew",
177535ac8abfatagen2026-10-10 10:39 UTC197
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
177535ac8abfatagen2026-10-10 10:39 UTC198
        )
177535ac8abfatagen2026-10-10 10:39 UTC199
        .into_response();
177535ac8abfatagen2026-10-10 10:39 UTC200
    }
177535ac8abfatagen2026-10-10 10:39 UTC201
    if method.as_str().eq_ignore_ascii_case("jelq") {
177535ac8abfatagen2026-10-10 10:39 UTC202
        return Problem::new(
177535ac8abfatagen2026-10-10 10:39 UTC203
            StatusCode::NOT_IMPLEMENTED,
177535ac8abfatagen2026-10-10 10:39 UTC204
            "not-implemented",
177535ac8abfatagen2026-10-10 10:39 UTC205
            format!(
177535ac8abfatagen2026-10-10 10:39 UTC206
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
177535ac8abfatagen2026-10-10 10:39 UTC207
            ),
177535ac8abfatagen2026-10-10 10:39 UTC208
        )
177535ac8abfatagen2026-10-10 10:39 UTC209
        .into_response();
177535ac8abfatagen2026-10-10 10:39 UTC210
    }
a9b016e4b248atagen2026-10-10 08:12 UTC211
    if !known {
a9b016e4b248atagen2026-10-10 08:12 UTC212
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC213
            StatusCode::NOT_IMPLEMENTED,
a9b016e4b248atagen2026-10-10 08:12 UTC214
            "not-implemented",
a9b016e4b248atagen2026-10-10 08:12 UTC215
            format!("The server does not implement {method}."),
a9b016e4b248atagen2026-10-10 08:12 UTC216
        )
a9b016e4b248atagen2026-10-10 08:12 UTC217
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC218
    }
a9b016e4b248atagen2026-10-10 08:12 UTC219
    Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC220
        StatusCode::METHOD_NOT_ALLOWED,
a9b016e4b248atagen2026-10-10 08:12 UTC221
        "method-not-allowed",
a9b016e4b248atagen2026-10-10 08:12 UTC222
        format!("{method} is not supported here."),
a9b016e4b248atagen2026-10-10 08:12 UTC223
    )
a9b016e4b248atagen2026-10-10 08:12 UTC224
    .header(header::ALLOW, allow)
a9b016e4b248atagen2026-10-10 08:12 UTC225
    .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC226
}
a9b016e4b248atagen2026-10-10 08:12 UTC227
a9b016e4b248atagen2026-10-10 08:12 UTC228
fn options(allow: &'static str, patchable: bool) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC229
    let mut response = (
a9b016e4b248atagen2026-10-10 08:12 UTC230
        StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC231
        [(header::ALLOW, allow)],
a9b016e4b248atagen2026-10-10 08:12 UTC232
        Json(json!({ "detail": THERAPY })),
a9b016e4b248atagen2026-10-10 08:12 UTC233
    )
a9b016e4b248atagen2026-10-10 08:12 UTC234
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC235
    if patchable {
a9b016e4b248atagen2026-10-10 08:12 UTC236
        response.headers_mut().insert(
a9b016e4b248atagen2026-10-10 08:12 UTC237
            "accept-patch",
a9b016e4b248atagen2026-10-10 08:12 UTC238
            HeaderValue::from_static("application/json-patch+json"),
a9b016e4b248atagen2026-10-10 08:12 UTC239
        );
a9b016e4b248atagen2026-10-10 08:12 UTC240
    }
a9b016e4b248atagen2026-10-10 08:12 UTC241
    response
a9b016e4b248atagen2026-10-10 08:12 UTC242
}
a9b016e4b248atagen2026-10-10 08:12 UTC243
a9b016e4b248atagen2026-10-10 08:12 UTC244
fn trace(method: &Method, uri: &Uri, version: Version, headers: &HeaderMap) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC245
    let mut message = format!("{method} {uri} {version:?}\r\n");
a9b016e4b248atagen2026-10-10 08:12 UTC246
    for (name, value) in headers {
a9b016e4b248atagen2026-10-10 08:12 UTC247
        let _ = write!(
a9b016e4b248atagen2026-10-10 08:12 UTC248
            message,
a9b016e4b248atagen2026-10-10 08:12 UTC249
            "{name}: {}\r\n",
a9b016e4b248atagen2026-10-10 08:12 UTC250
            value.to_str().unwrap_or("<binary>")
a9b016e4b248atagen2026-10-10 08:12 UTC251
        );
a9b016e4b248atagen2026-10-10 08:12 UTC252
    }
a9b016e4b248atagen2026-10-10 08:12 UTC253
    message.push_str("\r\n");
a9b016e4b248atagen2026-10-10 08:12 UTC254
    (
a9b016e4b248atagen2026-10-10 08:12 UTC255
        StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC256
        [(header::CONTENT_TYPE, "message/http")],
a9b016e4b248atagen2026-10-10 08:12 UTC257
        message,
a9b016e4b248atagen2026-10-10 08:12 UTC258
    )
a9b016e4b248atagen2026-10-10 08:12 UTC259
        .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC260
}
a9b016e4b248atagen2026-10-10 08:12 UTC261
a9b016e4b248atagen2026-10-10 08:12 UTC262
fn links(run: &Run) -> String {
a9b016e4b248atagen2026-10-10 08:12 UTC263
    let id = &run.id;
a9b016e4b248atagen2026-10-10 08:12 UTC264
    let mut links = vec![format!("</runs/{id}>; rel=\"{REL_BASE}run\"")];
a9b016e4b248atagen2026-10-10 08:12 UTC265
    for rel in [
dfa38cf4cd4batagen2026-10-10 09:17 UTC266
        "floor",
a9b016e4b248atagen2026-10-10 08:12 UTC267
        "length",
a9b016e4b248atagen2026-10-10 08:12 UTC268
        "foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC269
        "inventory",
a9b016e4b248atagen2026-10-10 08:12 UTC270
        "techniques",
a9b016e4b248atagen2026-10-10 08:12 UTC271
        "drawer",
dfa38cf4cd4batagen2026-10-10 09:17 UTC272
        "grip",
a9b016e4b248atagen2026-10-10 08:12 UTC273
        "diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC274
    ] {
a9b016e4b248atagen2026-10-10 08:12 UTC275
        links.push(format!("</runs/{id}/{rel}>; rel=\"{REL_BASE}{rel}\""));
a9b016e4b248atagen2026-10-10 08:12 UTC276
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC277
    if run.floor.shop_open() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC278
        links.push(format!("</runs/{id}/shop>; rel=\"{REL_BASE}shop\""));
dfa38cf4cd4batagen2026-10-10 09:17 UTC279
    }
a9b016e4b248atagen2026-10-10 08:12 UTC280
    links.join(", ")
a9b016e4b248atagen2026-10-10 08:12 UTC281
}
a9b016e4b248atagen2026-10-10 08:12 UTC282
a9b016e4b248atagen2026-10-10 08:12 UTC283
fn finish(run: &Run, mut response: Response) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC284
    let status = response.status();
dfa38cf4cd4batagen2026-10-10 09:17 UTC285
    if status == StatusCode::NOT_MODIFIED {
a9b016e4b248atagen2026-10-10 08:12 UTC286
        return response;
a9b016e4b248atagen2026-10-10 08:12 UTC287
    }
a9b016e4b248atagen2026-10-10 08:12 UTC288
    let mut stats = vec![
a9b016e4b248atagen2026-10-10 08:12 UTC289
        (
a9b016e4b248atagen2026-10-10 08:12 UTC290
            "x-self-reported-length",
a9b016e4b248atagen2026-10-10 08:12 UTC291
            format!("{:.2}", run.self_reported()),
a9b016e4b248atagen2026-10-10 08:12 UTC292
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC293
        ("x-girth", format!("{:.2}", run.girth)),
a9b016e4b248atagen2026-10-10 08:12 UTC294
        (
a9b016e4b248atagen2026-10-10 08:12 UTC295
            "x-foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC296
            run.foreskin.map_or("gone".into(), |f| format!("{f:.2}")),
a9b016e4b248atagen2026-10-10 08:12 UTC297
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC298
        ("x-eq", run.eq.to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC299
        ("x-stamina", format!("{}/{MAX_STAMINA}", run.stamina)),
a9b016e4b248atagen2026-10-10 08:12 UTC300
        (
a9b016e4b248atagen2026-10-10 08:12 UTC301
            "x-suspicion",
a9b016e4b248atagen2026-10-10 08:12 UTC302
            format!("{}/{SUSPICION_LIMIT}", run.suspicion),
a9b016e4b248atagen2026-10-10 08:12 UTC303
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC304
        ("x-shame", run.total_shame().to_string()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC305
        ("x-cash", run.cash.to_string()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC306
        ("x-floor", run.floor.number().to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC307
        ("link", links(run)),
a9b016e4b248atagen2026-10-10 08:12 UTC308
    ];
dfa38cf4cd4batagen2026-10-10 09:17 UTC309
    if let Some(observer) = run.observed {
dfa38cf4cd4batagen2026-10-10 09:17 UTC310
        stats.push(("x-observed-by", observer.into()));
a9b016e4b248atagen2026-10-10 08:12 UTC311
    }
a9b016e4b248atagen2026-10-10 08:12 UTC312
    if run.mom_present {
a9b016e4b248atagen2026-10-10 08:12 UTC313
        stats.push(("via", "1.1 mom".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC314
        stats.push(("vary", "Mom".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC315
    }
a9b016e4b248atagen2026-10-10 08:12 UTC316
    // RFC 9111 obsoleted Warning, which is why nobody heeds it.
a9b016e4b248atagen2026-10-10 08:12 UTC317
    if run.eq > 70 && run.ended.is_none() {
a9b016e4b248atagen2026-10-10 08:12 UTC318
        stats.push(("warning", "199 - \"it is turning purple\"".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC319
    }
a9b016e4b248atagen2026-10-10 08:12 UTC320
    stats.push((
a9b016e4b248atagen2026-10-10 08:12 UTC321
        "set-cookie",
a9b016e4b248atagen2026-10-10 08:12 UTC322
        format!("shame={}; {COOKIE_ATTRIBUTES}", run.total_shame()),
a9b016e4b248atagen2026-10-10 08:12 UTC323
    ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC324
    if run.items.ruler {
a9b016e4b248atagen2026-10-10 08:12 UTC325
        stats.push(("set-cookie", format!("ruler=1; {COOKIE_ATTRIBUTES}")));
a9b016e4b248atagen2026-10-10 08:12 UTC326
    }
a9b016e4b248atagen2026-10-10 08:12 UTC327
    let headers = response.headers_mut();
a9b016e4b248atagen2026-10-10 08:12 UTC328
    for (name, value) in stats {
a9b016e4b248atagen2026-10-10 08:12 UTC329
        headers.append(name, HeaderValue::from_str(&value).expect("visible ascii"));
a9b016e4b248atagen2026-10-10 08:12 UTC330
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC331
    let bodiless = status.is_informational() || status == StatusCode::NO_CONTENT;
dfa38cf4cd4batagen2026-10-10 09:17 UTC332
    if run.items.ring && !bodiless {
dfa38cf4cd4batagen2026-10-10 09:17 UTC333
        response.extensions_mut().insert(Compress);
dfa38cf4cd4batagen2026-10-10 09:17 UTC334
    }
a9b016e4b248atagen2026-10-10 08:12 UTC335
    response
a9b016e4b248atagen2026-10-10 08:12 UTC336
}
a9b016e4b248atagen2026-10-10 08:12 UTC337
a9b016e4b248atagen2026-10-10 08:12 UTC338
fn gone(run: &Run) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC339
    let ending = run.ended.as_ref().expect("only called on ended runs");
a9b016e4b248atagen2026-10-10 08:12 UTC340
    Problem::new(StatusCode::GONE, "run-ended", "This run has ended.")
a9b016e4b248atagen2026-10-10 08:12 UTC341
        .with("cause", format!("{PROBLEM_BASE}{}", ending.cause))
a9b016e4b248atagen2026-10-10 08:12 UTC342
        .with("turn", ending.turn)
a9b016e4b248atagen2026-10-10 08:12 UTC343
        .with("graveyard", "/graveyard")
a9b016e4b248atagen2026-10-10 08:12 UTC344
        .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC345
}
a9b016e4b248atagen2026-10-10 08:12 UTC346
dfa38cf4cd4batagen2026-10-10 09:17 UTC347
fn captive_portal(run: &Run) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC348
    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC349
        StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC350
        "captive-portal",
dfa38cf4cd4batagen2026-10-10 09:17 UTC351
        "The network requires authentication. The hotel would like you to agree to some terms.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC352
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC353
    .with("portal", format!("/runs/{}/portal", run.id))
dfa38cf4cd4batagen2026-10-10 09:17 UTC354
    .header(
dfa38cf4cd4batagen2026-10-10 09:17 UTC355
        header::LINK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC356
        format!("</runs/{}/portal>; rel=\"{REL_BASE}portal\"", run.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC357
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC358
    .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC359
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC360
a9b016e4b248atagen2026-10-10 08:12 UTC361
fn with_run(
a9b016e4b248atagen2026-10-10 08:12 UTC362
    state: &AppState,
a9b016e4b248atagen2026-10-10 08:12 UTC363
    id: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC364
    headers: &HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC365
    scope: Scope,
a9b016e4b248atagen2026-10-10 08:12 UTC366
    handle: impl FnOnce(&mut Run) -> Response,
a9b016e4b248atagen2026-10-10 08:12 UTC367
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC368
    let mut runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC369
    let Some(run) = runs.get_mut(id) else {
a9b016e4b248atagen2026-10-10 08:12 UTC370
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC371
            StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC372
            "no-such-run",
a9b016e4b248atagen2026-10-10 08:12 UTC373
            format!("There is no run {id}. There may never have been."),
a9b016e4b248atagen2026-10-10 08:12 UTC374
        )
a9b016e4b248atagen2026-10-10 08:12 UTC375
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC376
    };
a9b016e4b248atagen2026-10-10 08:12 UTC377
    if run.ended.is_some() {
a9b016e4b248atagen2026-10-10 08:12 UTC378
        return gone(run);
a9b016e4b248atagen2026-10-10 08:12 UTC379
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC380
    run.observed = None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC381
    if scope == Scope::Home {
dfa38cf4cd4batagen2026-10-10 09:17 UTC382
        if run.floor == Floor::Hotel && !run.wifi_accepted {
dfa38cf4cd4batagen2026-10-10 09:17 UTC383
            return finish(run, captive_portal(run));
dfa38cf4cd4batagen2026-10-10 09:17 UTC384
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC385
        if run.floor.has_mom()
dfa38cf4cd4batagen2026-10-10 09:17 UTC386
            && run.mom_present
dfa38cf4cd4batagen2026-10-10 09:17 UTC387
            && let Err(problem) = run.answer_door(header_str(headers, header::AUTHORIZATION))
dfa38cf4cd4batagen2026-10-10 09:17 UTC388
        {
dfa38cf4cd4batagen2026-10-10 09:17 UTC389
            return finish(run, problem.into_response());
dfa38cf4cd4batagen2026-10-10 09:17 UTC390
        }
a9b016e4b248atagen2026-10-10 08:12 UTC391
    }
a9b016e4b248atagen2026-10-10 08:12 UTC392
    let response = handle(run);
a9b016e4b248atagen2026-10-10 08:12 UTC393
    finish(run, response)
a9b016e4b248atagen2026-10-10 08:12 UTC394
}
a9b016e4b248atagen2026-10-10 08:12 UTC395
a9b016e4b248atagen2026-10-10 08:12 UTC396
fn respond<T: IntoResponse, E: IntoResponse>(result: Result<T, E>) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC397
    match result {
a9b016e4b248atagen2026-10-10 08:12 UTC398
        Ok(ok) => ok.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC399
        Err(err) => err.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC400
    }
a9b016e4b248atagen2026-10-10 08:12 UTC401
}
a9b016e4b248atagen2026-10-10 08:12 UTC402
a9b016e4b248atagen2026-10-10 08:12 UTC403
async fn runs(
a9b016e4b248atagen2026-10-10 08:12 UTC404
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC405
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC406
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC407
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC408
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC409
    if method != Method::POST {
a9b016e4b248atagen2026-10-10 08:12 UTC410
        return unsupported(&method, "POST");
a9b016e4b248atagen2026-10-10 08:12 UTC411
    }
a9b016e4b248atagen2026-10-10 08:12 UTC412
    let seed = match query.get("seed").map(|s| s.parse::<u64>()) {
a9b016e4b248atagen2026-10-10 08:12 UTC413
        None => rand::rng().random(),
a9b016e4b248atagen2026-10-10 08:12 UTC414
        Some(Ok(seed)) => seed,
a9b016e4b248atagen2026-10-10 08:12 UTC415
        Some(Err(_)) => {
a9b016e4b248atagen2026-10-10 08:12 UTC416
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC417
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC418
                "bad-seed",
a9b016e4b248atagen2026-10-10 08:12 UTC419
                "The seed must be an unsigned 64-bit integer.",
a9b016e4b248atagen2026-10-10 08:12 UTC420
            )
a9b016e4b248atagen2026-10-10 08:12 UTC421
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC422
        }
a9b016e4b248atagen2026-10-10 08:12 UTC423
    };
a9b016e4b248atagen2026-10-10 08:12 UTC424
    let shame = cookie(&headers, "shame")
a9b016e4b248atagen2026-10-10 08:12 UTC425
        .and_then(|s| s.parse().ok())
a9b016e4b248atagen2026-10-10 08:12 UTC426
        .unwrap_or(0);
a9b016e4b248atagen2026-10-10 08:12 UTC427
    let id = format!("{:08x}", rand::rng().random::<u32>());
a9b016e4b248atagen2026-10-10 08:12 UTC428
    let run = Run::new(id.clone(), seed, shame);
a9b016e4b248atagen2026-10-10 08:12 UTC429
    let response = (
a9b016e4b248atagen2026-10-10 08:12 UTC430
        StatusCode::CREATED,
a9b016e4b248atagen2026-10-10 08:12 UTC431
        [
a9b016e4b248atagen2026-10-10 08:12 UTC432
            (header::LOCATION, format!("/runs/{id}")),
a9b016e4b248atagen2026-10-10 08:12 UTC433
            (header::ETAG, run.etag()),
a9b016e4b248atagen2026-10-10 08:12 UTC434
            (HeaderName::from_static("x-seed"), seed.to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC435
        ],
a9b016e4b248atagen2026-10-10 08:12 UTC436
        Json(run.view()),
a9b016e4b248atagen2026-10-10 08:12 UTC437
    )
a9b016e4b248atagen2026-10-10 08:12 UTC438
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC439
    let response = finish(&run, response);
a9b016e4b248atagen2026-10-10 08:12 UTC440
    state.insert(run);
a9b016e4b248atagen2026-10-10 08:12 UTC441
    response
a9b016e4b248atagen2026-10-10 08:12 UTC442
}
a9b016e4b248atagen2026-10-10 08:12 UTC443
a9b016e4b248atagen2026-10-10 08:12 UTC444
#[allow(clippy::too_many_arguments)]
a9b016e4b248atagen2026-10-10 08:12 UTC445
async fn run(
a9b016e4b248atagen2026-10-10 08:12 UTC446
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC447
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC448
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC449
    uri: Uri,
a9b016e4b248atagen2026-10-10 08:12 UTC450
    version: Version,
a9b016e4b248atagen2026-10-10 08:12 UTC451
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC452
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC453
    body: Bytes,
a9b016e4b248atagen2026-10-10 08:12 UTC454
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC455
    let now = Instant::now();
dfa38cf4cd4batagen2026-10-10 09:17 UTC456
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC457
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC458
            "GET" | "HEAD" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
                let accept = header_str(&headers, header::ACCEPT).unwrap_or_default();
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
                if accept.contains("reassurance") {
dfa38cf4cd4batagen2026-10-10 09:17 UTC461
                    return Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
                        StatusCode::NOT_ACCEPTABLE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
                        "no-reassurance",
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
                        "The server does not produce reassurance.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
                    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC466
                    .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC467
                }
dfa38cf4cd4batagen2026-10-10 09:17 UTC468
                (
dfa38cf4cd4batagen2026-10-10 09:17 UTC469
                    StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC470
                    [(header::ETAG, run.etag())],
dfa38cf4cd4batagen2026-10-10 09:17 UTC471
                    Json(run.view()),
a9b016e4b248atagen2026-10-10 08:12 UTC472
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC473
                    .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC474
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC475
            "JELQ" => jelq(run, &query, &headers, now),
dfa38cf4cd4batagen2026-10-10 09:17 UTC476
            "PUT" => run.put().into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC477
            "PATCH" => patch(run, &headers, &body),
dfa38cf4cd4batagen2026-10-10 09:17 UTC478
            "DELETE" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC479
                run.end("abandoned", "The run was abandoned.");
dfa38cf4cd4batagen2026-10-10 09:17 UTC480
                StatusCode::NO_CONTENT.into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC481
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC482
            "OPTIONS" => options(RUN_ALLOW, true),
dfa38cf4cd4batagen2026-10-10 09:17 UTC483
            "TRACE" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC484
                run.shame += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC485
                trace(&method, &uri, version, &headers)
dfa38cf4cd4batagen2026-10-10 09:17 UTC486
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC487
            _ => unsupported(&method, RUN_ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC488
        }
a9b016e4b248atagen2026-10-10 08:12 UTC489
    })
a9b016e4b248atagen2026-10-10 08:12 UTC490
}
a9b016e4b248atagen2026-10-10 08:12 UTC491
a9b016e4b248atagen2026-10-10 08:12 UTC492
fn jelq(
a9b016e4b248atagen2026-10-10 08:12 UTC493
    run: &mut Run,
a9b016e4b248atagen2026-10-10 08:12 UTC494
    query: &HashMap<String, String>,
a9b016e4b248atagen2026-10-10 08:12 UTC495
    headers: &HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC496
    now: Instant,
a9b016e4b248atagen2026-10-10 08:12 UTC497
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC498
    let techniques_href = format!("/runs/{}/techniques", run.id);
177535ac8abfatagen2026-10-10 10:39 UTC499
    let Some(names) = query.get("technique").filter(|n| !n.trim().is_empty()) else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
        return Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
            StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC502
            "technique-required",
dfa38cf4cd4batagen2026-10-10 09:17 UTC503
            "No technique was specified.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC504
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC505
        .with("techniques", techniques_href)
dfa38cf4cd4batagen2026-10-10 09:17 UTC506
        .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC507
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC508
    let mut techniques = Vec::new();
177535ac8abfatagen2026-10-10 10:39 UTC509
    for name in names.split(',').map(str::trim).filter(|n| !n.is_empty()) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC510
        let Some(technique) = Technique::parse(name) else {
a9b016e4b248atagen2026-10-10 08:12 UTC511
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC512
                StatusCode::BAD_REQUEST,
dfa38cf4cd4batagen2026-10-10 09:17 UTC513
                "unknown-technique",
dfa38cf4cd4batagen2026-10-10 09:17 UTC514
                format!("{name} is not a technique."),
a9b016e4b248atagen2026-10-10 08:12 UTC515
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC516
            .with("techniques", techniques_href)
a9b016e4b248atagen2026-10-10 08:12 UTC517
            .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC518
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC519
        techniques.push(technique);
dfa38cf4cd4batagen2026-10-10 09:17 UTC520
    }
a9b016e4b248atagen2026-10-10 08:12 UTC521
    let intensity = match query.get("intensity").map(|i| i.parse::<i32>()) {
a9b016e4b248atagen2026-10-10 08:12 UTC522
        None => 5,
a9b016e4b248atagen2026-10-10 08:12 UTC523
        Some(Ok(i)) if (1..=10).contains(&i) => i,
a9b016e4b248atagen2026-10-10 08:12 UTC524
        Some(_) => {
a9b016e4b248atagen2026-10-10 08:12 UTC525
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC526
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC527
                "intensity-out-of-range",
a9b016e4b248atagen2026-10-10 08:12 UTC528
                "Intensity is measured from 1 to 10.",
a9b016e4b248atagen2026-10-10 08:12 UTC529
            )
a9b016e4b248atagen2026-10-10 08:12 UTC530
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC531
        }
a9b016e4b248atagen2026-10-10 08:12 UTC532
    };
a9b016e4b248atagen2026-10-10 08:12 UTC533
    let lube = match Lube::from_content_type(header_str(headers, header::CONTENT_TYPE)) {
a9b016e4b248atagen2026-10-10 08:12 UTC534
        Ok(lube) => lube,
a9b016e4b248atagen2026-10-10 08:12 UTC535
        Err(problem) => return problem.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC536
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC537
    respond(run.jelq(&techniques, intensity, lube, now))
a9b016e4b248atagen2026-10-10 08:12 UTC538
}
a9b016e4b248atagen2026-10-10 08:12 UTC539
a9b016e4b248atagen2026-10-10 08:12 UTC540
fn patch(run: &mut Run, headers: &HeaderMap, body: &[u8]) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC541
    let content_type = header_str(headers, header::CONTENT_TYPE).unwrap_or_default();
a9b016e4b248atagen2026-10-10 08:12 UTC542
    if !content_type.starts_with("application/json-patch+json") {
a9b016e4b248atagen2026-10-10 08:12 UTC543
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC544
            StatusCode::UNSUPPORTED_MEDIA_TYPE,
a9b016e4b248atagen2026-10-10 08:12 UTC545
            "patch-format",
a9b016e4b248atagen2026-10-10 08:12 UTC546
            "Patches are accepted as application/json-patch+json.",
a9b016e4b248atagen2026-10-10 08:12 UTC547
        )
a9b016e4b248atagen2026-10-10 08:12 UTC548
        .header(
a9b016e4b248atagen2026-10-10 08:12 UTC549
            HeaderName::from_static("accept-patch"),
a9b016e4b248atagen2026-10-10 08:12 UTC550
            "application/json-patch+json",
a9b016e4b248atagen2026-10-10 08:12 UTC551
        )
a9b016e4b248atagen2026-10-10 08:12 UTC552
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC553
    }
a9b016e4b248atagen2026-10-10 08:12 UTC554
    match serde_json::from_slice::<Vec<PatchOp>>(body) {
a9b016e4b248atagen2026-10-10 08:12 UTC555
        Ok(ops) => respond(run.patch(&ops)),
a9b016e4b248atagen2026-10-10 08:12 UTC556
        Err(_) => Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC557
            StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC558
            "malformed-patch",
a9b016e4b248atagen2026-10-10 08:12 UTC559
            "The patch could not be parsed.",
a9b016e4b248atagen2026-10-10 08:12 UTC560
        )
a9b016e4b248atagen2026-10-10 08:12 UTC561
        .into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC562
    }
a9b016e4b248atagen2026-10-10 08:12 UTC563
}
a9b016e4b248atagen2026-10-10 08:12 UTC564
dfa38cf4cd4batagen2026-10-10 09:17 UTC565
fn millimetres(inches: f64) -> usize {
dfa38cf4cd4batagen2026-10-10 09:17 UTC566
    (inches * MM_PER_INCH).round() as usize
dfa38cf4cd4batagen2026-10-10 09:17 UTC567
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC568
dfa38cf4cd4batagen2026-10-10 09:17 UTC569
fn parse_inch_range(range: &str, total: f64) -> Option<(f64, f64)> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC570
    let (start, end) = range.strip_prefix("inches=")?.split_once('-')?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC571
    let start: f64 = start.trim().parse().ok()?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC572
    let end: f64 = match end.trim() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC573
        "" => total,
dfa38cf4cd4batagen2026-10-10 09:17 UTC574
        end => end.parse().ok()?,
dfa38cf4cd4batagen2026-10-10 09:17 UTC575
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC576
    (start >= 0.0 && start < total && start <= end).then_some((start, end.min(total)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC577
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC578
dfa38cf4cd4batagen2026-10-10 09:17 UTC579
fn length_response(run: &Run, headers: &HeaderMap) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC580
    let inches = if run.items.ruler {
dfa38cf4cd4batagen2026-10-10 09:17 UTC581
        run.current_length()
dfa38cf4cd4batagen2026-10-10 09:17 UTC582
    } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC583
        run.self_reported()
dfa38cf4cd4batagen2026-10-10 09:17 UTC584
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC585
    let body = shaft(millimetres(inches));
dfa38cf4cd4batagen2026-10-10 09:17 UTC586
    let content_type = (
dfa38cf4cd4batagen2026-10-10 09:17 UTC587
        header::CONTENT_TYPE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC588
        "text/plain; charset=us-ascii".to_owned(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC589
    );
dfa38cf4cd4batagen2026-10-10 09:17 UTC590
    let accept_ranges = (header::ACCEPT_RANGES, "inches".to_owned());
dfa38cf4cd4batagen2026-10-10 09:17 UTC591
dfa38cf4cd4batagen2026-10-10 09:17 UTC592
    if let Some(range) = header_str(headers, header::RANGE)
dfa38cf4cd4batagen2026-10-10 09:17 UTC593
        && range.starts_with("inches=")
dfa38cf4cd4batagen2026-10-10 09:17 UTC594
    {
dfa38cf4cd4batagen2026-10-10 09:17 UTC595
        let Some((start, end)) = parse_inch_range(range, inches) else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC596
            return Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC597
                StatusCode::RANGE_NOT_SATISFIABLE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC598
                "range-not-satisfiable",
dfa38cf4cd4batagen2026-10-10 09:17 UTC599
                format!("The requested range is not available. {inches:.2} inches are."),
dfa38cf4cd4batagen2026-10-10 09:17 UTC600
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC601
            .header(header::CONTENT_RANGE, format!("inches */{inches:.2}"))
dfa38cf4cd4batagen2026-10-10 09:17 UTC602
            .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC603
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC604
        let from = millimetres(start).min(body.len() - 1);
dfa38cf4cd4batagen2026-10-10 09:17 UTC605
        let to = millimetres(end).clamp(from + 1, body.len());
dfa38cf4cd4batagen2026-10-10 09:17 UTC606
        let content_range = (
dfa38cf4cd4batagen2026-10-10 09:17 UTC607
            header::CONTENT_RANGE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC608
            format!("inches {start:.2}-{end:.2}/{inches:.2}"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC609
        );
dfa38cf4cd4batagen2026-10-10 09:17 UTC610
        return (
dfa38cf4cd4batagen2026-10-10 09:17 UTC611
            StatusCode::PARTIAL_CONTENT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC612
            [content_type, accept_ranges, content_range],
dfa38cf4cd4batagen2026-10-10 09:17 UTC613
            body[from..to].to_owned(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC614
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC615
            .into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC616
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC617
dfa38cf4cd4batagen2026-10-10 09:17 UTC618
    if run.items.ruler {
dfa38cf4cd4batagen2026-10-10 09:17 UTC619
        return (StatusCode::OK, [content_type, accept_ranges], body).into_response();
dfa38cf4cd4batagen2026-10-10 09:17 UTC620
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC621
    let chunks: Vec<Result<Bytes, Infallible>> = body
dfa38cf4cd4batagen2026-10-10 09:17 UTC622
        .into_bytes()
dfa38cf4cd4batagen2026-10-10 09:17 UTC623
        .chunks(16)
dfa38cf4cd4batagen2026-10-10 09:17 UTC624
        .map(|c| Ok(Bytes::copy_from_slice(c)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC625
        .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC626
    let body = Body::from_stream(futures_util::stream::iter(chunks));
dfa38cf4cd4batagen2026-10-10 09:17 UTC627
    (StatusCode::OK, [content_type, accept_ranges], body).into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC628
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC629
a9b016e4b248atagen2026-10-10 08:12 UTC630
async fn length(
a9b016e4b248atagen2026-10-10 08:12 UTC631
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC632
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC633
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC634
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC635
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC636
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC637
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC638
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC639
            "GET" | "HEAD" => length_response(run, &headers),
dfa38cf4cd4batagen2026-10-10 09:17 UTC640
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC641
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC642
        }
a9b016e4b248atagen2026-10-10 08:12 UTC643
    })
a9b016e4b248atagen2026-10-10 08:12 UTC644
}
a9b016e4b248atagen2026-10-10 08:12 UTC645
a9b016e4b248atagen2026-10-10 08:12 UTC646
async fn foreskin(
a9b016e4b248atagen2026-10-10 08:12 UTC647
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC648
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC649
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC650
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC651
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC652
    const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC653
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC654
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC655
            "GET" | "HEAD" => match run.foreskin {
dfa38cf4cd4batagen2026-10-10 09:17 UTC656
                Some(f) => (
dfa38cf4cd4batagen2026-10-10 09:17 UTC657
                    StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC658
                    [(header::ETAG, run.etag())],
dfa38cf4cd4batagen2026-10-10 09:17 UTC659
                    Json(json!({ "foreskin": (f * 100.0).round() / 100.0 })),
dfa38cf4cd4batagen2026-10-10 09:17 UTC660
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC661
                    .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC662
                None => Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC663
                    StatusCode::GONE,
dfa38cf4cd4batagen2026-10-10 09:17 UTC664
                    "foreskin-gone",
dfa38cf4cd4batagen2026-10-10 09:17 UTC665
                    "The foreskin is gone. It will not be returning.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC666
                )
a9b016e4b248atagen2026-10-10 08:12 UTC667
                .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC668
            },
dfa38cf4cd4batagen2026-10-10 09:17 UTC669
            "DELETE" => respond(run.delete_foreskin(header_str(&headers, header::IF_MATCH))),
dfa38cf4cd4batagen2026-10-10 09:17 UTC670
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC671
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC672
        }
a9b016e4b248atagen2026-10-10 08:12 UTC673
    })
a9b016e4b248atagen2026-10-10 08:12 UTC674
}
a9b016e4b248atagen2026-10-10 08:12 UTC675
a9b016e4b248atagen2026-10-10 08:12 UTC676
async fn inventory(
a9b016e4b248atagen2026-10-10 08:12 UTC677
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC678
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC679
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC680
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC681
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC682
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC683
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC684
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC685
            "GET" | "HEAD" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC686
                let listed: Vec<Value> = run
dfa38cf4cd4batagen2026-10-10 09:17 UTC687
                    .inventory()
dfa38cf4cd4batagen2026-10-10 09:17 UTC688
                    .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC689
                    .map(|(name, _)| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC690
                        json!({ "name": name, "href": format!("/runs/{}/inventory/{name}", run.id) })
dfa38cf4cd4batagen2026-10-10 09:17 UTC691
                    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC692
                    .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC693
                Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC694
                    "items": listed,
dfa38cf4cd4batagen2026-10-10 09:17 UTC695
                    "ice_packs": run.items.ice_packs,
dfa38cf4cd4batagen2026-10-10 09:17 UTC696
                    "cash": run.cash,
dfa38cf4cd4batagen2026-10-10 09:17 UTC697
                    "excuses": run.excuses(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC698
                }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC699
                .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC700
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC701
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC702
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC703
        }
a9b016e4b248atagen2026-10-10 08:12 UTC704
    })
a9b016e4b248atagen2026-10-10 08:12 UTC705
}
a9b016e4b248atagen2026-10-10 08:12 UTC706
a9b016e4b248atagen2026-10-10 08:12 UTC707
async fn item(
a9b016e4b248atagen2026-10-10 08:12 UTC708
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC709
    Path((id, name)): Path<(String, String)>,
a9b016e4b248atagen2026-10-10 08:12 UTC710
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC711
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC712
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC713
    const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC714
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC715
        let Some((name, description)) = run.inventory().into_iter().find(|(n, _)| *n == name)
dfa38cf4cd4batagen2026-10-10 09:17 UTC716
        else {
a9b016e4b248atagen2026-10-10 08:12 UTC717
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC718
                StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC719
                "no-such-item",
a9b016e4b248atagen2026-10-10 08:12 UTC720
                "You do not have that.",
a9b016e4b248atagen2026-10-10 08:12 UTC721
            )
a9b016e4b248atagen2026-10-10 08:12 UTC722
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC723
        };
a9b016e4b248atagen2026-10-10 08:12 UTC724
        match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC725
            "GET" | "HEAD" => {
a9b016e4b248atagen2026-10-10 08:12 UTC726
                Json(json!({ "name": name, "description": description })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC727
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC728
            "DELETE" => respond(run.drop_item(name)),
a9b016e4b248atagen2026-10-10 08:12 UTC729
            "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 UTC730
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC731
        }
a9b016e4b248atagen2026-10-10 08:12 UTC732
    })
a9b016e4b248atagen2026-10-10 08:12 UTC733
}
a9b016e4b248atagen2026-10-10 08:12 UTC734
a9b016e4b248atagen2026-10-10 08:12 UTC735
async fn techniques(
a9b016e4b248atagen2026-10-10 08:12 UTC736
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC737
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC738
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC739
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC740
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC741
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC742
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC743
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC744
            "GET" | "HEAD" => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC745
                let listed: Vec<Value> = Technique::ALL
dfa38cf4cd4batagen2026-10-10 09:17 UTC746
                    .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC747
                    .map(|t| t.describe(run.forum_read))
dfa38cf4cd4batagen2026-10-10 09:17 UTC748
                    .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC749
                Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC750
                    "usage": format!("JELQ /runs/{}?technique=<name>[,<name>]&intensity=<1-10>", run.id),
dfa38cf4cd4batagen2026-10-10 09:17 UTC751
                    "lubricant": "Declared with Content-Type.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC752
                    "lubricants": LUBRICANTS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC753
                    "eq_band": [50, 70],
dfa38cf4cd4batagen2026-10-10 09:17 UTC754
                    "techniques": listed,
dfa38cf4cd4batagen2026-10-10 09:17 UTC755
                }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC756
                .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC757
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC758
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC759
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC760
        }
a9b016e4b248atagen2026-10-10 08:12 UTC761
    })
a9b016e4b248atagen2026-10-10 08:12 UTC762
}
a9b016e4b248atagen2026-10-10 08:12 UTC763
a9b016e4b248atagen2026-10-10 08:12 UTC764
async fn drawer(
a9b016e4b248atagen2026-10-10 08:12 UTC765
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC766
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC767
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC768
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC769
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC770
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC771
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC772
            "POST" => respond(run.search_drawer()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC773
            "OPTIONS" => options("POST, OPTIONS", false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC774
            _ => unsupported(&method, "POST, OPTIONS"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC775
        }
a9b016e4b248atagen2026-10-10 08:12 UTC776
    })
a9b016e4b248atagen2026-10-10 08:12 UTC777
}
a9b016e4b248atagen2026-10-10 08:12 UTC778
a9b016e4b248atagen2026-10-10 08:12 UTC779
async fn branch(
a9b016e4b248atagen2026-10-10 08:12 UTC780
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC781
    Path((id, choice)): Path<(String, String)>,
a9b016e4b248atagen2026-10-10 08:12 UTC782
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC783
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC784
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC785
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC786
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC787
            "POST" => respond(run.choose(&choice)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC788
            "OPTIONS" => options("POST, OPTIONS", false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC789
            _ => unsupported(&method, "POST, OPTIONS"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC790
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC791
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC792
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC793
dfa38cf4cd4batagen2026-10-10 09:17 UTC794
async fn floor(
dfa38cf4cd4batagen2026-10-10 09:17 UTC795
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC796
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC797
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC798
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC799
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC800
    const ALLOW: &str = "GET, HEAD, POST, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC801
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC802
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC803
            "GET" | "HEAD" => Json(run.floor_view()).into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC804
            "POST" => respond(run.advance()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC805
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC806
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC807
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC808
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC809
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC810
dfa38cf4cd4batagen2026-10-10 09:17 UTC811
async fn shop(
dfa38cf4cd4batagen2026-10-10 09:17 UTC812
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC813
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC814
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC815
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC816
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC817
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC818
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC819
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC820
            "GET" | "HEAD" => respond(run.shop_view().map(Json)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC821
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC822
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC823
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC824
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC825
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC826
dfa38cf4cd4batagen2026-10-10 09:17 UTC827
async fn buy(
dfa38cf4cd4batagen2026-10-10 09:17 UTC828
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC829
    Path((id, item)): Path<(String, String)>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC830
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC831
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC832
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC833
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC834
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC835
            "POST" => respond(run.buy(&item)),
dfa38cf4cd4batagen2026-10-10 09:17 UTC836
            "OPTIONS" => options("POST, OPTIONS", false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC837
            _ => unsupported(&method, "POST, OPTIONS"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC838
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC839
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC840
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC841
dfa38cf4cd4batagen2026-10-10 09:17 UTC842
async fn portal(
dfa38cf4cd4batagen2026-10-10 09:17 UTC843
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC844
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC845
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC846
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC847
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC848
    const ALLOW: &str = "GET, HEAD, POST, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC849
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC850
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC851
            "GET" | "HEAD" => Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC852
                "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 UTC853
                "accept": { "href": format!("/runs/{}/portal", run.id), "method": "POST" },
dfa38cf4cd4batagen2026-10-10 09:17 UTC854
            }))
dfa38cf4cd4batagen2026-10-10 09:17 UTC855
            .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC856
            "POST" => respond(run.accept_terms()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC857
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC858
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC859
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC860
    })
dfa38cf4cd4batagen2026-10-10 09:17 UTC861
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC862
dfa38cf4cd4batagen2026-10-10 09:17 UTC863
async fn elder(
dfa38cf4cd4batagen2026-10-10 09:17 UTC864
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC865
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC866
    method: Method,
dfa38cf4cd4batagen2026-10-10 09:17 UTC867
    headers: HeaderMap,
dfa38cf4cd4batagen2026-10-10 09:17 UTC868
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC869
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC870
    with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC871
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC872
            "GET" | "HEAD" => respond(run.elder(
dfa38cf4cd4batagen2026-10-10 09:17 UTC873
                header_str(&headers, header::PROXY_AUTHORIZATION),
dfa38cf4cd4batagen2026-10-10 09:17 UTC874
                header_str(&headers, header::IF_MODIFIED_SINCE),
dfa38cf4cd4batagen2026-10-10 09:17 UTC875
            )),
dfa38cf4cd4batagen2026-10-10 09:17 UTC876
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC877
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC878
        }
a9b016e4b248atagen2026-10-10 08:12 UTC879
    })
a9b016e4b248atagen2026-10-10 08:12 UTC880
}
a9b016e4b248atagen2026-10-10 08:12 UTC881
dfa38cf4cd4batagen2026-10-10 09:17 UTC882
fn requested_grip(headers: &HeaderMap) -> Option<Grip> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC883
    let connection = header_str(headers, header::CONNECTION)?.to_ascii_lowercase();
dfa38cf4cd4batagen2026-10-10 09:17 UTC884
    if !connection.split(',').any(|token| token.trim() == "upgrade") {
dfa38cf4cd4batagen2026-10-10 09:17 UTC885
        return None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC886
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC887
    header_str(headers, header::UPGRADE)?
dfa38cf4cd4batagen2026-10-10 09:17 UTC888
        .split(',')
dfa38cf4cd4batagen2026-10-10 09:17 UTC889
        .find_map(|protocol| Grip::parse(protocol.trim()))
dfa38cf4cd4batagen2026-10-10 09:17 UTC890
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC891
dfa38cf4cd4batagen2026-10-10 09:17 UTC892
async fn grip(
dfa38cf4cd4batagen2026-10-10 09:17 UTC893
    State(state): State<Shared>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC894
    Path(id): Path<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC895
    mut request: Request,
dfa38cf4cd4batagen2026-10-10 09:17 UTC896
) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC897
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC898
    let method = request.method().clone();
dfa38cf4cd4batagen2026-10-10 09:17 UTC899
    let headers = request.headers().clone();
dfa38cf4cd4batagen2026-10-10 09:17 UTC900
    let wanted = requested_grip(&headers);
dfa38cf4cd4batagen2026-10-10 09:17 UTC901
    let upgrade = wanted.map(|_| hyper::upgrade::on(&mut request));
177535ac8abfatagen2026-10-10 10:39 UTC902
    let detail = match header_str(&headers, header::UPGRADE) {
177535ac8abfatagen2026-10-10 10:39 UTC903
        Some(protocol) if wanted.is_none() => {
177535ac8abfatagen2026-10-10 10:39 UTC904
            format!("The server does not know the {protocol} grip. Grips are protocols.")
177535ac8abfatagen2026-10-10 10:39 UTC905
        }
177535ac8abfatagen2026-10-10 10:39 UTC906
        _ => "Grips are protocols. Change grip by upgrading the connection.".to_owned(),
177535ac8abfatagen2026-10-10 10:39 UTC907
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC908
    let response = with_run(&state, &id, &headers, Scope::Home, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC909
        match (method.as_str(), wanted) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC910
            ("GET" | "HEAD", Some(grip)) => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC911
                run.set_grip(grip);
dfa38cf4cd4batagen2026-10-10 09:17 UTC912
                (
dfa38cf4cd4batagen2026-10-10 09:17 UTC913
                    StatusCode::SWITCHING_PROTOCOLS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC914
                    [
dfa38cf4cd4batagen2026-10-10 09:17 UTC915
                        (header::UPGRADE, grip.name()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC916
                        (header::CONNECTION, "upgrade"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC917
                    ],
dfa38cf4cd4batagen2026-10-10 09:17 UTC918
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC919
                    .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC920
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC921
            ("GET" | "HEAD", None) => (
dfa38cf4cd4batagen2026-10-10 09:17 UTC922
                StatusCode::OK,
dfa38cf4cd4batagen2026-10-10 09:17 UTC923
                [(header::UPGRADE, "overhand, underhand")],
dfa38cf4cd4batagen2026-10-10 09:17 UTC924
                Json(json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC925
                    "grip": run.grip.name(),
177535ac8abfatagen2026-10-10 10:39 UTC926
                    "detail": detail,
dfa38cf4cd4batagen2026-10-10 09:17 UTC927
                })),
dfa38cf4cd4batagen2026-10-10 09:17 UTC928
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC929
                .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC930
            ("OPTIONS", _) => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC931
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC932
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC933
    });
dfa38cf4cd4batagen2026-10-10 09:17 UTC934
    if response.status() == StatusCode::SWITCHING_PROTOCOLS
dfa38cf4cd4batagen2026-10-10 09:17 UTC935
        && let (Some(upgrade), Some(grip)) = (upgrade, wanted)
dfa38cf4cd4batagen2026-10-10 09:17 UTC936
    {
dfa38cf4cd4batagen2026-10-10 09:17 UTC937
        tokio::spawn(async move {
dfa38cf4cd4batagen2026-10-10 09:17 UTC938
            if let Ok(upgraded) = upgrade.await {
dfa38cf4cd4batagen2026-10-10 09:17 UTC939
                let mut io = TokioIo::new(upgraded);
dfa38cf4cd4batagen2026-10-10 09:17 UTC940
                let line = format!("You are now jelqing {}.\r\n", grip.name());
dfa38cf4cd4batagen2026-10-10 09:17 UTC941
                let _ = io.write_all(line.as_bytes()).await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC942
                let _ = io.shutdown().await;
dfa38cf4cd4batagen2026-10-10 09:17 UTC943
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC944
        });
dfa38cf4cd4batagen2026-10-10 09:17 UTC945
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC946
    response
dfa38cf4cd4batagen2026-10-10 09:17 UTC947
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC948
a9b016e4b248atagen2026-10-10 08:12 UTC949
async fn diagnosis(
a9b016e4b248atagen2026-10-10 08:12 UTC950
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC951
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC952
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC953
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC954
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC955
    const ALLOW: &str = "GET, HEAD, OPTIONS";
dfa38cf4cd4batagen2026-10-10 09:17 UTC956
    with_run(&state, &id, &headers, Scope::Pockets, |run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC957
        match method.as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC958
            "GET" | "HEAD" => respond(run.diagnose(
dfa38cf4cd4batagen2026-10-10 09:17 UTC959
                header_str(&headers, header::AUTHORIZATION),
dfa38cf4cd4batagen2026-10-10 09:17 UTC960
                header_str(&headers, header::IF_NONE_MATCH),
dfa38cf4cd4batagen2026-10-10 09:17 UTC961
            )),
dfa38cf4cd4batagen2026-10-10 09:17 UTC962
            "OPTIONS" => options(ALLOW, false),
dfa38cf4cd4batagen2026-10-10 09:17 UTC963
            _ => unsupported(&method, ALLOW),
dfa38cf4cd4batagen2026-10-10 09:17 UTC964
        }
a9b016e4b248atagen2026-10-10 08:12 UTC965
    })
a9b016e4b248atagen2026-10-10 08:12 UTC966
}
a9b016e4b248atagen2026-10-10 08:12 UTC967
dfa38cf4cd4batagen2026-10-10 09:17 UTC968
async fn thread(Path(year): Path<String>, method: Method) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC969
    if method != Method::GET && method != Method::HEAD {
dfa38cf4cd4batagen2026-10-10 09:17 UTC970
        return unsupported(&method, "GET, HEAD");
dfa38cf4cd4batagen2026-10-10 09:17 UTC971
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC972
    match year.parse::<u32>() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC973
        Ok(2009) => (
dfa38cf4cd4batagen2026-10-10 09:17 UTC974
            [(header::LAST_MODIFIED, ELDER_LAST_MODIFIED)],
dfa38cf4cd4batagen2026-10-10 09:17 UTC975
            Json(forum_thread()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC976
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC977
            .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC978
        Ok(y) if y > 2009 => Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC979
            StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC980
            "no-thread",
dfa38cf4cd4batagen2026-10-10 09:17 UTC981
            "Nobody has posted since 2009.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC982
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC983
        .with("thread", FORUM_THREAD)
dfa38cf4cd4batagen2026-10-10 09:17 UTC984
        .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC985
        _ => Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC986
            StatusCode::NOT_FOUND,
dfa38cf4cd4batagen2026-10-10 09:17 UTC987
            "no-thread",
dfa38cf4cd4batagen2026-10-10 09:17 UTC988
            "The forum did not exist yet. The problem did.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC989
        )
dfa38cf4cd4batagen2026-10-10 09:17 UTC990
        .with("thread", FORUM_THREAD)
dfa38cf4cd4batagen2026-10-10 09:17 UTC991
        .into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC992
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC993
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC994
a9b016e4b248atagen2026-10-10 08:12 UTC995
async fn graveyard(State(state): State<Shared>, method: Method) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC996
    if method != Method::GET && method != Method::HEAD {
a9b016e4b248atagen2026-10-10 08:12 UTC997
        return unsupported(&method, "GET, HEAD");
a9b016e4b248atagen2026-10-10 08:12 UTC998
    }
a9b016e4b248atagen2026-10-10 08:12 UTC999
    let runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC1000
    let dead: Vec<Value> = runs
a9b016e4b248atagen2026-10-10 08:12 UTC1001
        .values()
a9b016e4b248atagen2026-10-10 08:12 UTC1002
        .filter_map(|run| {
a9b016e4b248atagen2026-10-10 08:12 UTC1003
            let ending = run.ended.as_ref()?;
a9b016e4b248atagen2026-10-10 08:12 UTC1004
            Some(json!({
a9b016e4b248atagen2026-10-10 08:12 UTC1005
                "id": run.id,
a9b016e4b248atagen2026-10-10 08:12 UTC1006
                "cause": format!("{PROBLEM_BASE}{}", ending.cause),
a9b016e4b248atagen2026-10-10 08:12 UTC1007
                "detail": ending.detail,
a9b016e4b248atagen2026-10-10 08:12 UTC1008
                "turn": ending.turn,
a9b016e4b248atagen2026-10-10 08:12 UTC1009
            }))
a9b016e4b248atagen2026-10-10 08:12 UTC1010
        })
a9b016e4b248atagen2026-10-10 08:12 UTC1011
        .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC1012
    Json(json!({ "runs": dead })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC1013
}
a9b016e4b248atagen2026-10-10 08:12 UTC1014
a9b016e4b248atagen2026-10-10 08:12 UTC1015
async fn leaderboard(
a9b016e4b248atagen2026-10-10 08:12 UTC1016
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC1017
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC1018
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC1019
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC1020
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC1021
    if method != Method::GET && method != Method::HEAD {
a9b016e4b248atagen2026-10-10 08:12 UTC1022
        return unsupported(&method, "GET, HEAD");
a9b016e4b248atagen2026-10-10 08:12 UTC1023
    }
a9b016e4b248atagen2026-10-10 08:12 UTC1024
    let truthful = match query.get("sort").map(String::as_str) {
a9b016e4b248atagen2026-10-10 08:12 UTC1025
        None | Some("self-reported-length") => false,
a9b016e4b248atagen2026-10-10 08:12 UTC1026
        Some("content-length") if cookie(&headers, "ruler") == Some("1") => true,
a9b016e4b248atagen2026-10-10 08:12 UTC1027
        Some("content-length") => {
a9b016e4b248atagen2026-10-10 08:12 UTC1028
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC1029
                StatusCode::FORBIDDEN,
a9b016e4b248atagen2026-10-10 08:12 UTC1030
                "ruler-required",
a9b016e4b248atagen2026-10-10 08:12 UTC1031
                "Sorting by the truth requires a Ruler.",
a9b016e4b248atagen2026-10-10 08:12 UTC1032
            )
a9b016e4b248atagen2026-10-10 08:12 UTC1033
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC1034
        }
a9b016e4b248atagen2026-10-10 08:12 UTC1035
        Some(_) => {
a9b016e4b248atagen2026-10-10 08:12 UTC1036
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC1037
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC1038
                "unknown-sort",
a9b016e4b248atagen2026-10-10 08:12 UTC1039
                "The leaderboard sorts by self-reported-length or content-length.",
a9b016e4b248atagen2026-10-10 08:12 UTC1040
            )
a9b016e4b248atagen2026-10-10 08:12 UTC1041
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC1042
        }
a9b016e4b248atagen2026-10-10 08:12 UTC1043
    };
a9b016e4b248atagen2026-10-10 08:12 UTC1044
    let runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC1045
    let mut entries: Vec<&Run> = runs.values().collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC1046
    let key = |r: &Run| {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1047
        if truthful {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1048
            r.current_length()
dfa38cf4cd4batagen2026-10-10 09:17 UTC1049
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1050
            r.self_reported()
dfa38cf4cd4batagen2026-10-10 09:17 UTC1051
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC1052
    };
dfa38cf4cd4batagen2026-10-10 09:17 UTC1053
    entries.sort_by(|a, b| key(b).total_cmp(&key(a)));
a9b016e4b248atagen2026-10-10 08:12 UTC1054
    let entries: Vec<Value> = entries
a9b016e4b248atagen2026-10-10 08:12 UTC1055
        .into_iter()
a9b016e4b248atagen2026-10-10 08:12 UTC1056
        .map(|run| {
a9b016e4b248atagen2026-10-10 08:12 UTC1057
            let mut entry = json!({
a9b016e4b248atagen2026-10-10 08:12 UTC1058
                "id": run.id,
a9b016e4b248atagen2026-10-10 08:12 UTC1059
                "status": run.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC1060
                "floor": run.floor.number(),
a9b016e4b248atagen2026-10-10 08:12 UTC1061
                "self_reported_length": (run.self_reported() * 100.0).round() / 100.0,
a9b016e4b248atagen2026-10-10 08:12 UTC1062
            });
a9b016e4b248atagen2026-10-10 08:12 UTC1063
            if truthful {
dfa38cf4cd4batagen2026-10-10 09:17 UTC1064
                entry["content_length"] = json!(millimetres(run.current_length()));
a9b016e4b248atagen2026-10-10 08:12 UTC1065
            }
a9b016e4b248atagen2026-10-10 08:12 UTC1066
            entry
a9b016e4b248atagen2026-10-10 08:12 UTC1067
        })
a9b016e4b248atagen2026-10-10 08:12 UTC1068
        .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC1069
    Json(json!({ "entries": entries })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC1070
}
a9b016e4b248atagen2026-10-10 08:12 UTC1071
a9b016e4b248atagen2026-10-10 08:12 UTC1072
async fn not_found() -> Problem {
a9b016e4b248atagen2026-10-10 08:12 UTC1073
    Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC1074
        StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC1075
        "not-found",
a9b016e4b248atagen2026-10-10 08:12 UTC1076
        "The resource does not exist. This is not about you.",
a9b016e4b248atagen2026-10-10 08:12 UTC1077
    )
a9b016e4b248atagen2026-10-10 08:12 UTC1078
}