atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
a9b016e4b248ebfb4326a7881143856a1fe77252
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 _,
a9b016e4b248atagen2026-10-10 08:12 UTC5
    sync::{Arc, Mutex},
a9b016e4b248atagen2026-10-10 08:12 UTC6
    time::Instant,
a9b016e4b248atagen2026-10-10 08:12 UTC7
};
a9b016e4b248atagen2026-10-10 08:12 UTC8
a9b016e4b248atagen2026-10-10 08:12 UTC9
use axum::{
a9b016e4b248atagen2026-10-10 08:12 UTC10
    Json, Router,
a9b016e4b248atagen2026-10-10 08:12 UTC11
    body::{Body, Bytes},
a9b016e4b248atagen2026-10-10 08:12 UTC12
    extract::{Path, Query, Request, State},
a9b016e4b248atagen2026-10-10 08:12 UTC13
    http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri, Version, header},
a9b016e4b248atagen2026-10-10 08:12 UTC14
    middleware::{self, Next},
a9b016e4b248atagen2026-10-10 08:12 UTC15
    response::{IntoResponse, Response},
a9b016e4b248atagen2026-10-10 08:12 UTC16
    routing::any,
a9b016e4b248atagen2026-10-10 08:12 UTC17
};
a9b016e4b248atagen2026-10-10 08:12 UTC18
use rand::Rng;
a9b016e4b248atagen2026-10-10 08:12 UTC19
use serde_json::{Value, json};
a9b016e4b248atagen2026-10-10 08:12 UTC20
a9b016e4b248atagen2026-10-10 08:12 UTC21
use crate::{
a9b016e4b248atagen2026-10-10 08:12 UTC22
    game::{
a9b016e4b248atagen2026-10-10 08:12 UTC23
        EXCUSES, LUBRICANTS, Lube, MAX_STAMINA, MM_PER_INCH, PatchOp, Run, SUSPICION_LIMIT,
a9b016e4b248atagen2026-10-10 08:12 UTC24
        Technique, shaft,
a9b016e4b248atagen2026-10-10 08:12 UTC25
    },
a9b016e4b248atagen2026-10-10 08:12 UTC26
    problem::{PROBLEM_BASE, Problem},
a9b016e4b248atagen2026-10-10 08:12 UTC27
};
a9b016e4b248atagen2026-10-10 08:12 UTC28
a9b016e4b248atagen2026-10-10 08:12 UTC29
const REL_BASE: &str = "https://jelq.dev/rels/";
a9b016e4b248atagen2026-10-10 08:12 UTC30
const COOKIE_ATTRIBUTES: &str = "Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict";
a9b016e4b248atagen2026-10-10 08:12 UTC31
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.";
a9b016e4b248atagen2026-10-10 08:12 UTC32
a9b016e4b248atagen2026-10-10 08:12 UTC33
#[derive(Default)]
a9b016e4b248atagen2026-10-10 08:12 UTC34
pub struct AppState {
a9b016e4b248atagen2026-10-10 08:12 UTC35
    runs: Mutex<HashMap<String, Run>>,
a9b016e4b248atagen2026-10-10 08:12 UTC36
}
a9b016e4b248atagen2026-10-10 08:12 UTC37
a9b016e4b248atagen2026-10-10 08:12 UTC38
impl AppState {
a9b016e4b248atagen2026-10-10 08:12 UTC39
    pub fn insert(&self, run: Run) {
a9b016e4b248atagen2026-10-10 08:12 UTC40
        self.runs.lock().unwrap().insert(run.id.clone(), run);
a9b016e4b248atagen2026-10-10 08:12 UTC41
    }
a9b016e4b248atagen2026-10-10 08:12 UTC42
}
a9b016e4b248atagen2026-10-10 08:12 UTC43
a9b016e4b248atagen2026-10-10 08:12 UTC44
type Shared = Arc<AppState>;
a9b016e4b248atagen2026-10-10 08:12 UTC45
a9b016e4b248atagen2026-10-10 08:12 UTC46
pub fn app() -> Router {
a9b016e4b248atagen2026-10-10 08:12 UTC47
    router(Arc::default())
a9b016e4b248atagen2026-10-10 08:12 UTC48
}
a9b016e4b248atagen2026-10-10 08:12 UTC49
a9b016e4b248atagen2026-10-10 08:12 UTC50
pub fn router(state: Shared) -> Router {
a9b016e4b248atagen2026-10-10 08:12 UTC51
    Router::new()
a9b016e4b248atagen2026-10-10 08:12 UTC52
        .route("/runs", any(runs))
a9b016e4b248atagen2026-10-10 08:12 UTC53
        .route("/runs/{id}", any(run))
a9b016e4b248atagen2026-10-10 08:12 UTC54
        .route("/runs/{id}/length", any(length))
a9b016e4b248atagen2026-10-10 08:12 UTC55
        .route("/runs/{id}/foreskin", any(foreskin))
a9b016e4b248atagen2026-10-10 08:12 UTC56
        .route("/runs/{id}/inventory", any(inventory))
a9b016e4b248atagen2026-10-10 08:12 UTC57
        .route("/runs/{id}/inventory/{item}", any(item))
a9b016e4b248atagen2026-10-10 08:12 UTC58
        .route("/runs/{id}/techniques", any(techniques))
a9b016e4b248atagen2026-10-10 08:12 UTC59
        .route("/runs/{id}/drawer", any(drawer))
a9b016e4b248atagen2026-10-10 08:12 UTC60
        .route("/runs/{id}/branches/{choice}", any(branch))
a9b016e4b248atagen2026-10-10 08:12 UTC61
        .route("/runs/{id}/diagnosis", any(diagnosis))
a9b016e4b248atagen2026-10-10 08:12 UTC62
        .route("/graveyard", any(graveyard))
a9b016e4b248atagen2026-10-10 08:12 UTC63
        .route("/leaderboard", any(leaderboard))
a9b016e4b248atagen2026-10-10 08:12 UTC64
        .fallback(not_found)
a9b016e4b248atagen2026-10-10 08:12 UTC65
        .layer(middleware::from_fn(chrome))
a9b016e4b248atagen2026-10-10 08:12 UTC66
        .with_state(state)
a9b016e4b248atagen2026-10-10 08:12 UTC67
}
a9b016e4b248atagen2026-10-10 08:12 UTC68
a9b016e4b248atagen2026-10-10 08:12 UTC69
async fn chrome(request: Request, next: Next) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC70
    let mut response = next.run(request).await;
a9b016e4b248atagen2026-10-10 08:12 UTC71
    let headers = response.headers_mut();
a9b016e4b248atagen2026-10-10 08:12 UTC72
    headers.insert(header::SERVER, HeaderValue::from_static("jelqd/0.6.9"));
a9b016e4b248atagen2026-10-10 08:12 UTC73
    headers.insert("x-powered-by", HeaderValue::from_static("blood"));
a9b016e4b248atagen2026-10-10 08:12 UTC74
    headers.insert(
a9b016e4b248atagen2026-10-10 08:12 UTC75
        header::CACHE_CONTROL,
a9b016e4b248atagen2026-10-10 08:12 UTC76
        HeaderValue::from_static("no-store, private"),
a9b016e4b248atagen2026-10-10 08:12 UTC77
    );
a9b016e4b248atagen2026-10-10 08:12 UTC78
    response
a9b016e4b248atagen2026-10-10 08:12 UTC79
}
a9b016e4b248atagen2026-10-10 08:12 UTC80
a9b016e4b248atagen2026-10-10 08:12 UTC81
fn header_str(headers: &HeaderMap, name: HeaderName) -> Option<&str> {
a9b016e4b248atagen2026-10-10 08:12 UTC82
    headers.get(name).and_then(|v| v.to_str().ok())
a9b016e4b248atagen2026-10-10 08:12 UTC83
}
a9b016e4b248atagen2026-10-10 08:12 UTC84
a9b016e4b248atagen2026-10-10 08:12 UTC85
fn cookie<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
a9b016e4b248atagen2026-10-10 08:12 UTC86
    headers
a9b016e4b248atagen2026-10-10 08:12 UTC87
        .get_all(header::COOKIE)
a9b016e4b248atagen2026-10-10 08:12 UTC88
        .iter()
a9b016e4b248atagen2026-10-10 08:12 UTC89
        .filter_map(|v| v.to_str().ok())
a9b016e4b248atagen2026-10-10 08:12 UTC90
        .flat_map(|v| v.split(';'))
a9b016e4b248atagen2026-10-10 08:12 UTC91
        .filter_map(|pair| pair.trim().split_once('='))
a9b016e4b248atagen2026-10-10 08:12 UTC92
        .find(|(key, _)| *key == name)
a9b016e4b248atagen2026-10-10 08:12 UTC93
        .map(|(_, value)| value)
a9b016e4b248atagen2026-10-10 08:12 UTC94
}
a9b016e4b248atagen2026-10-10 08:12 UTC95
a9b016e4b248atagen2026-10-10 08:12 UTC96
fn unsupported(method: &Method, allow: &'static str) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC97
    if *method == Method::CONNECT {
a9b016e4b248atagen2026-10-10 08:12 UTC98
        return StatusCode::NOT_IMPLEMENTED.into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC99
    }
a9b016e4b248atagen2026-10-10 08:12 UTC100
    let known = matches!(
a9b016e4b248atagen2026-10-10 08:12 UTC101
        method.as_str(),
a9b016e4b248atagen2026-10-10 08:12 UTC102
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
a9b016e4b248atagen2026-10-10 08:12 UTC103
    );
a9b016e4b248atagen2026-10-10 08:12 UTC104
    if !known {
a9b016e4b248atagen2026-10-10 08:12 UTC105
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC106
            StatusCode::NOT_IMPLEMENTED,
a9b016e4b248atagen2026-10-10 08:12 UTC107
            "not-implemented",
a9b016e4b248atagen2026-10-10 08:12 UTC108
            format!("The server does not implement {method}."),
a9b016e4b248atagen2026-10-10 08:12 UTC109
        )
a9b016e4b248atagen2026-10-10 08:12 UTC110
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC111
    }
a9b016e4b248atagen2026-10-10 08:12 UTC112
    Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC113
        StatusCode::METHOD_NOT_ALLOWED,
a9b016e4b248atagen2026-10-10 08:12 UTC114
        "method-not-allowed",
a9b016e4b248atagen2026-10-10 08:12 UTC115
        format!("{method} is not supported here."),
a9b016e4b248atagen2026-10-10 08:12 UTC116
    )
a9b016e4b248atagen2026-10-10 08:12 UTC117
    .header(header::ALLOW, allow)
a9b016e4b248atagen2026-10-10 08:12 UTC118
    .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC119
}
a9b016e4b248atagen2026-10-10 08:12 UTC120
a9b016e4b248atagen2026-10-10 08:12 UTC121
fn options(allow: &'static str, patchable: bool) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC122
    let mut response = (
a9b016e4b248atagen2026-10-10 08:12 UTC123
        StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC124
        [(header::ALLOW, allow)],
a9b016e4b248atagen2026-10-10 08:12 UTC125
        Json(json!({ "detail": THERAPY })),
a9b016e4b248atagen2026-10-10 08:12 UTC126
    )
a9b016e4b248atagen2026-10-10 08:12 UTC127
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC128
    if patchable {
a9b016e4b248atagen2026-10-10 08:12 UTC129
        response.headers_mut().insert(
a9b016e4b248atagen2026-10-10 08:12 UTC130
            "accept-patch",
a9b016e4b248atagen2026-10-10 08:12 UTC131
            HeaderValue::from_static("application/json-patch+json"),
a9b016e4b248atagen2026-10-10 08:12 UTC132
        );
a9b016e4b248atagen2026-10-10 08:12 UTC133
    }
a9b016e4b248atagen2026-10-10 08:12 UTC134
    response
a9b016e4b248atagen2026-10-10 08:12 UTC135
}
a9b016e4b248atagen2026-10-10 08:12 UTC136
a9b016e4b248atagen2026-10-10 08:12 UTC137
fn trace(method: &Method, uri: &Uri, version: Version, headers: &HeaderMap) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC138
    let mut message = format!("{method} {uri} {version:?}\r\n");
a9b016e4b248atagen2026-10-10 08:12 UTC139
    for (name, value) in headers {
a9b016e4b248atagen2026-10-10 08:12 UTC140
        let _ = write!(
a9b016e4b248atagen2026-10-10 08:12 UTC141
            message,
a9b016e4b248atagen2026-10-10 08:12 UTC142
            "{name}: {}\r\n",
a9b016e4b248atagen2026-10-10 08:12 UTC143
            value.to_str().unwrap_or("<binary>")
a9b016e4b248atagen2026-10-10 08:12 UTC144
        );
a9b016e4b248atagen2026-10-10 08:12 UTC145
    }
a9b016e4b248atagen2026-10-10 08:12 UTC146
    message.push_str("\r\n");
a9b016e4b248atagen2026-10-10 08:12 UTC147
    (
a9b016e4b248atagen2026-10-10 08:12 UTC148
        StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC149
        [(header::CONTENT_TYPE, "message/http")],
a9b016e4b248atagen2026-10-10 08:12 UTC150
        message,
a9b016e4b248atagen2026-10-10 08:12 UTC151
    )
a9b016e4b248atagen2026-10-10 08:12 UTC152
        .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC153
}
a9b016e4b248atagen2026-10-10 08:12 UTC154
a9b016e4b248atagen2026-10-10 08:12 UTC155
fn links(run: &Run) -> String {
a9b016e4b248atagen2026-10-10 08:12 UTC156
    let id = &run.id;
a9b016e4b248atagen2026-10-10 08:12 UTC157
    let mut links = vec![format!("</runs/{id}>; rel=\"{REL_BASE}run\"")];
a9b016e4b248atagen2026-10-10 08:12 UTC158
    for rel in [
a9b016e4b248atagen2026-10-10 08:12 UTC159
        "length",
a9b016e4b248atagen2026-10-10 08:12 UTC160
        "foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC161
        "inventory",
a9b016e4b248atagen2026-10-10 08:12 UTC162
        "techniques",
a9b016e4b248atagen2026-10-10 08:12 UTC163
        "drawer",
a9b016e4b248atagen2026-10-10 08:12 UTC164
        "diagnosis",
a9b016e4b248atagen2026-10-10 08:12 UTC165
    ] {
a9b016e4b248atagen2026-10-10 08:12 UTC166
        links.push(format!("</runs/{id}/{rel}>; rel=\"{REL_BASE}{rel}\""));
a9b016e4b248atagen2026-10-10 08:12 UTC167
    }
a9b016e4b248atagen2026-10-10 08:12 UTC168
    links.join(", ")
a9b016e4b248atagen2026-10-10 08:12 UTC169
}
a9b016e4b248atagen2026-10-10 08:12 UTC170
a9b016e4b248atagen2026-10-10 08:12 UTC171
fn finish(run: &Run, mut response: Response) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC172
    if response.status() == StatusCode::NOT_MODIFIED {
a9b016e4b248atagen2026-10-10 08:12 UTC173
        return response;
a9b016e4b248atagen2026-10-10 08:12 UTC174
    }
a9b016e4b248atagen2026-10-10 08:12 UTC175
    let mut stats = vec![
a9b016e4b248atagen2026-10-10 08:12 UTC176
        (
a9b016e4b248atagen2026-10-10 08:12 UTC177
            "x-self-reported-length",
a9b016e4b248atagen2026-10-10 08:12 UTC178
            format!("{:.2}", run.self_reported()),
a9b016e4b248atagen2026-10-10 08:12 UTC179
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC180
        ("x-girth", format!("{:.2}", run.girth)),
a9b016e4b248atagen2026-10-10 08:12 UTC181
        (
a9b016e4b248atagen2026-10-10 08:12 UTC182
            "x-foreskin",
a9b016e4b248atagen2026-10-10 08:12 UTC183
            run.foreskin.map_or("gone".into(), |f| format!("{f:.2}")),
a9b016e4b248atagen2026-10-10 08:12 UTC184
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC185
        ("x-eq", run.eq.to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC186
        ("x-stamina", format!("{}/{MAX_STAMINA}", run.stamina)),
a9b016e4b248atagen2026-10-10 08:12 UTC187
        (
a9b016e4b248atagen2026-10-10 08:12 UTC188
            "x-suspicion",
a9b016e4b248atagen2026-10-10 08:12 UTC189
            format!("{}/{SUSPICION_LIMIT}", run.suspicion),
a9b016e4b248atagen2026-10-10 08:12 UTC190
        ),
a9b016e4b248atagen2026-10-10 08:12 UTC191
        ("x-shame", run.total_shame().to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC192
        ("link", links(run)),
a9b016e4b248atagen2026-10-10 08:12 UTC193
    ];
a9b016e4b248atagen2026-10-10 08:12 UTC194
    if run.observed {
a9b016e4b248atagen2026-10-10 08:12 UTC195
        stats.push(("x-observed-by", "cat".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC196
    }
a9b016e4b248atagen2026-10-10 08:12 UTC197
    if run.mom_present {
a9b016e4b248atagen2026-10-10 08:12 UTC198
        stats.push(("via", "1.1 mom".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC199
        stats.push(("vary", "Mom".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC200
    }
a9b016e4b248atagen2026-10-10 08:12 UTC201
    // RFC 9111 obsoleted Warning, which is why nobody heeds it.
a9b016e4b248atagen2026-10-10 08:12 UTC202
    if run.eq > 70 && run.ended.is_none() {
a9b016e4b248atagen2026-10-10 08:12 UTC203
        stats.push(("warning", "199 - \"it is turning purple\"".into()));
a9b016e4b248atagen2026-10-10 08:12 UTC204
    }
a9b016e4b248atagen2026-10-10 08:12 UTC205
    stats.push((
a9b016e4b248atagen2026-10-10 08:12 UTC206
        "set-cookie",
a9b016e4b248atagen2026-10-10 08:12 UTC207
        format!("shame={}; {COOKIE_ATTRIBUTES}", run.total_shame()),
a9b016e4b248atagen2026-10-10 08:12 UTC208
    ));
a9b016e4b248atagen2026-10-10 08:12 UTC209
    if run.ruler {
a9b016e4b248atagen2026-10-10 08:12 UTC210
        stats.push(("set-cookie", format!("ruler=1; {COOKIE_ATTRIBUTES}")));
a9b016e4b248atagen2026-10-10 08:12 UTC211
    }
a9b016e4b248atagen2026-10-10 08:12 UTC212
    let headers = response.headers_mut();
a9b016e4b248atagen2026-10-10 08:12 UTC213
    for (name, value) in stats {
a9b016e4b248atagen2026-10-10 08:12 UTC214
        headers.append(name, HeaderValue::from_str(&value).expect("visible ascii"));
a9b016e4b248atagen2026-10-10 08:12 UTC215
    }
a9b016e4b248atagen2026-10-10 08:12 UTC216
    response
a9b016e4b248atagen2026-10-10 08:12 UTC217
}
a9b016e4b248atagen2026-10-10 08:12 UTC218
a9b016e4b248atagen2026-10-10 08:12 UTC219
fn gone(run: &Run) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC220
    let ending = run.ended.as_ref().expect("only called on ended runs");
a9b016e4b248atagen2026-10-10 08:12 UTC221
    Problem::new(StatusCode::GONE, "run-ended", "This run has ended.")
a9b016e4b248atagen2026-10-10 08:12 UTC222
        .with("cause", format!("{PROBLEM_BASE}{}", ending.cause))
a9b016e4b248atagen2026-10-10 08:12 UTC223
        .with("turn", ending.turn)
a9b016e4b248atagen2026-10-10 08:12 UTC224
        .with("graveyard", "/graveyard")
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 with_run(
a9b016e4b248atagen2026-10-10 08:12 UTC229
    state: &AppState,
a9b016e4b248atagen2026-10-10 08:12 UTC230
    id: &str,
a9b016e4b248atagen2026-10-10 08:12 UTC231
    headers: &HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC232
    mom_applies: bool,
a9b016e4b248atagen2026-10-10 08:12 UTC233
    handle: impl FnOnce(&mut Run) -> Response,
a9b016e4b248atagen2026-10-10 08:12 UTC234
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC235
    let mut runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC236
    let Some(run) = runs.get_mut(id) else {
a9b016e4b248atagen2026-10-10 08:12 UTC237
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC238
            StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC239
            "no-such-run",
a9b016e4b248atagen2026-10-10 08:12 UTC240
            format!("There is no run {id}. There may never have been."),
a9b016e4b248atagen2026-10-10 08:12 UTC241
        )
a9b016e4b248atagen2026-10-10 08:12 UTC242
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC243
    };
a9b016e4b248atagen2026-10-10 08:12 UTC244
    if run.ended.is_some() {
a9b016e4b248atagen2026-10-10 08:12 UTC245
        return gone(run);
a9b016e4b248atagen2026-10-10 08:12 UTC246
    }
a9b016e4b248atagen2026-10-10 08:12 UTC247
    run.observed = false;
a9b016e4b248atagen2026-10-10 08:12 UTC248
    if mom_applies
a9b016e4b248atagen2026-10-10 08:12 UTC249
        && run.mom_present
a9b016e4b248atagen2026-10-10 08:12 UTC250
        && let Err(problem) = run.answer_door(header_str(headers, header::AUTHORIZATION))
a9b016e4b248atagen2026-10-10 08:12 UTC251
    {
a9b016e4b248atagen2026-10-10 08:12 UTC252
        return finish(run, problem.into_response());
a9b016e4b248atagen2026-10-10 08:12 UTC253
    }
a9b016e4b248atagen2026-10-10 08:12 UTC254
    let response = handle(run);
a9b016e4b248atagen2026-10-10 08:12 UTC255
    finish(run, response)
a9b016e4b248atagen2026-10-10 08:12 UTC256
}
a9b016e4b248atagen2026-10-10 08:12 UTC257
a9b016e4b248atagen2026-10-10 08:12 UTC258
fn respond<T: IntoResponse, E: IntoResponse>(result: Result<T, E>) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC259
    match result {
a9b016e4b248atagen2026-10-10 08:12 UTC260
        Ok(ok) => ok.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC261
        Err(err) => err.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC262
    }
a9b016e4b248atagen2026-10-10 08:12 UTC263
}
a9b016e4b248atagen2026-10-10 08:12 UTC264
a9b016e4b248atagen2026-10-10 08:12 UTC265
async fn runs(
a9b016e4b248atagen2026-10-10 08:12 UTC266
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC267
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC268
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC269
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC270
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC271
    if method != Method::POST {
a9b016e4b248atagen2026-10-10 08:12 UTC272
        return unsupported(&method, "POST");
a9b016e4b248atagen2026-10-10 08:12 UTC273
    }
a9b016e4b248atagen2026-10-10 08:12 UTC274
    let seed = match query.get("seed").map(|s| s.parse::<u64>()) {
a9b016e4b248atagen2026-10-10 08:12 UTC275
        None => rand::rng().random(),
a9b016e4b248atagen2026-10-10 08:12 UTC276
        Some(Ok(seed)) => seed,
a9b016e4b248atagen2026-10-10 08:12 UTC277
        Some(Err(_)) => {
a9b016e4b248atagen2026-10-10 08:12 UTC278
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC279
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC280
                "bad-seed",
a9b016e4b248atagen2026-10-10 08:12 UTC281
                "The seed must be an unsigned 64-bit integer.",
a9b016e4b248atagen2026-10-10 08:12 UTC282
            )
a9b016e4b248atagen2026-10-10 08:12 UTC283
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC284
        }
a9b016e4b248atagen2026-10-10 08:12 UTC285
    };
a9b016e4b248atagen2026-10-10 08:12 UTC286
    let shame = cookie(&headers, "shame")
a9b016e4b248atagen2026-10-10 08:12 UTC287
        .and_then(|s| s.parse().ok())
a9b016e4b248atagen2026-10-10 08:12 UTC288
        .unwrap_or(0);
a9b016e4b248atagen2026-10-10 08:12 UTC289
    let id = format!("{:08x}", rand::rng().random::<u32>());
a9b016e4b248atagen2026-10-10 08:12 UTC290
    let run = Run::new(id.clone(), seed, shame);
a9b016e4b248atagen2026-10-10 08:12 UTC291
    let response = (
a9b016e4b248atagen2026-10-10 08:12 UTC292
        StatusCode::CREATED,
a9b016e4b248atagen2026-10-10 08:12 UTC293
        [
a9b016e4b248atagen2026-10-10 08:12 UTC294
            (header::LOCATION, format!("/runs/{id}")),
a9b016e4b248atagen2026-10-10 08:12 UTC295
            (header::ETAG, run.etag()),
a9b016e4b248atagen2026-10-10 08:12 UTC296
            (HeaderName::from_static("x-seed"), seed.to_string()),
a9b016e4b248atagen2026-10-10 08:12 UTC297
        ],
a9b016e4b248atagen2026-10-10 08:12 UTC298
        Json(run.view()),
a9b016e4b248atagen2026-10-10 08:12 UTC299
    )
a9b016e4b248atagen2026-10-10 08:12 UTC300
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC301
    let response = finish(&run, response);
a9b016e4b248atagen2026-10-10 08:12 UTC302
    state.insert(run);
a9b016e4b248atagen2026-10-10 08:12 UTC303
    response
a9b016e4b248atagen2026-10-10 08:12 UTC304
}
a9b016e4b248atagen2026-10-10 08:12 UTC305
a9b016e4b248atagen2026-10-10 08:12 UTC306
#[allow(clippy::too_many_arguments)]
a9b016e4b248atagen2026-10-10 08:12 UTC307
async fn run(
a9b016e4b248atagen2026-10-10 08:12 UTC308
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC309
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC310
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC311
    uri: Uri,
a9b016e4b248atagen2026-10-10 08:12 UTC312
    version: Version,
a9b016e4b248atagen2026-10-10 08:12 UTC313
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC314
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC315
    body: Bytes,
a9b016e4b248atagen2026-10-10 08:12 UTC316
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC317
    const ALLOW: &str = "GET, HEAD, JELQ, PUT, PATCH, DELETE, OPTIONS, TRACE";
a9b016e4b248atagen2026-10-10 08:12 UTC318
    let now = Instant::now();
a9b016e4b248atagen2026-10-10 08:12 UTC319
    with_run(&state, &id, &headers, true, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC320
        "GET" | "HEAD" => {
a9b016e4b248atagen2026-10-10 08:12 UTC321
            let accept = header_str(&headers, header::ACCEPT).unwrap_or_default();
a9b016e4b248atagen2026-10-10 08:12 UTC322
            if accept.contains("reassurance") {
a9b016e4b248atagen2026-10-10 08:12 UTC323
                return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC324
                    StatusCode::NOT_ACCEPTABLE,
a9b016e4b248atagen2026-10-10 08:12 UTC325
                    "no-reassurance",
a9b016e4b248atagen2026-10-10 08:12 UTC326
                    "The server does not produce reassurance.",
a9b016e4b248atagen2026-10-10 08:12 UTC327
                )
a9b016e4b248atagen2026-10-10 08:12 UTC328
                .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC329
            }
a9b016e4b248atagen2026-10-10 08:12 UTC330
            (
a9b016e4b248atagen2026-10-10 08:12 UTC331
                StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC332
                [(header::ETAG, run.etag())],
a9b016e4b248atagen2026-10-10 08:12 UTC333
                Json(run.view()),
a9b016e4b248atagen2026-10-10 08:12 UTC334
            )
a9b016e4b248atagen2026-10-10 08:12 UTC335
                .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC336
        }
a9b016e4b248atagen2026-10-10 08:12 UTC337
        "JELQ" => jelq(run, &query, &headers, now),
a9b016e4b248atagen2026-10-10 08:12 UTC338
        "PUT" => run.put().into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC339
        "PATCH" => patch(run, &headers, &body),
a9b016e4b248atagen2026-10-10 08:12 UTC340
        "DELETE" => {
a9b016e4b248atagen2026-10-10 08:12 UTC341
            run.end("abandoned", "The run was abandoned.");
a9b016e4b248atagen2026-10-10 08:12 UTC342
            StatusCode::NO_CONTENT.into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC343
        }
a9b016e4b248atagen2026-10-10 08:12 UTC344
        "OPTIONS" => options(ALLOW, true),
a9b016e4b248atagen2026-10-10 08:12 UTC345
        "TRACE" => {
a9b016e4b248atagen2026-10-10 08:12 UTC346
            run.shame += 1;
a9b016e4b248atagen2026-10-10 08:12 UTC347
            trace(&method, &uri, version, &headers)
a9b016e4b248atagen2026-10-10 08:12 UTC348
        }
a9b016e4b248atagen2026-10-10 08:12 UTC349
        _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC350
    })
a9b016e4b248atagen2026-10-10 08:12 UTC351
}
a9b016e4b248atagen2026-10-10 08:12 UTC352
a9b016e4b248atagen2026-10-10 08:12 UTC353
fn jelq(
a9b016e4b248atagen2026-10-10 08:12 UTC354
    run: &mut Run,
a9b016e4b248atagen2026-10-10 08:12 UTC355
    query: &HashMap<String, String>,
a9b016e4b248atagen2026-10-10 08:12 UTC356
    headers: &HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC357
    now: Instant,
a9b016e4b248atagen2026-10-10 08:12 UTC358
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC359
    let technique = match query.get("technique") {
a9b016e4b248atagen2026-10-10 08:12 UTC360
        None => {
a9b016e4b248atagen2026-10-10 08:12 UTC361
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC362
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC363
                "technique-required",
a9b016e4b248atagen2026-10-10 08:12 UTC364
                "No technique was specified.",
a9b016e4b248atagen2026-10-10 08:12 UTC365
            )
a9b016e4b248atagen2026-10-10 08:12 UTC366
            .with("techniques", format!("/runs/{}/techniques", run.id))
a9b016e4b248atagen2026-10-10 08:12 UTC367
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC368
        }
a9b016e4b248atagen2026-10-10 08:12 UTC369
        Some(name) => match Technique::parse(name) {
a9b016e4b248atagen2026-10-10 08:12 UTC370
            Some(t) => t,
a9b016e4b248atagen2026-10-10 08:12 UTC371
            None => {
a9b016e4b248atagen2026-10-10 08:12 UTC372
                return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC373
                    StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC374
                    "unknown-technique",
a9b016e4b248atagen2026-10-10 08:12 UTC375
                    format!("{name} is not a technique."),
a9b016e4b248atagen2026-10-10 08:12 UTC376
                )
a9b016e4b248atagen2026-10-10 08:12 UTC377
                .with("techniques", format!("/runs/{}/techniques", run.id))
a9b016e4b248atagen2026-10-10 08:12 UTC378
                .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC379
            }
a9b016e4b248atagen2026-10-10 08:12 UTC380
        },
a9b016e4b248atagen2026-10-10 08:12 UTC381
    };
a9b016e4b248atagen2026-10-10 08:12 UTC382
    let intensity = match query.get("intensity").map(|i| i.parse::<i32>()) {
a9b016e4b248atagen2026-10-10 08:12 UTC383
        None => 5,
a9b016e4b248atagen2026-10-10 08:12 UTC384
        Some(Ok(i)) if (1..=10).contains(&i) => i,
a9b016e4b248atagen2026-10-10 08:12 UTC385
        Some(_) => {
a9b016e4b248atagen2026-10-10 08:12 UTC386
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC387
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC388
                "intensity-out-of-range",
a9b016e4b248atagen2026-10-10 08:12 UTC389
                "Intensity is measured from 1 to 10.",
a9b016e4b248atagen2026-10-10 08:12 UTC390
            )
a9b016e4b248atagen2026-10-10 08:12 UTC391
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC392
        }
a9b016e4b248atagen2026-10-10 08:12 UTC393
    };
a9b016e4b248atagen2026-10-10 08:12 UTC394
    let lube = match Lube::from_content_type(header_str(headers, header::CONTENT_TYPE)) {
a9b016e4b248atagen2026-10-10 08:12 UTC395
        Ok(lube) => lube,
a9b016e4b248atagen2026-10-10 08:12 UTC396
        Err(problem) => return problem.into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC397
    };
a9b016e4b248atagen2026-10-10 08:12 UTC398
    respond(run.jelq(technique, intensity, lube, now))
a9b016e4b248atagen2026-10-10 08:12 UTC399
}
a9b016e4b248atagen2026-10-10 08:12 UTC400
a9b016e4b248atagen2026-10-10 08:12 UTC401
fn patch(run: &mut Run, headers: &HeaderMap, body: &[u8]) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC402
    let content_type = header_str(headers, header::CONTENT_TYPE).unwrap_or_default();
a9b016e4b248atagen2026-10-10 08:12 UTC403
    if !content_type.starts_with("application/json-patch+json") {
a9b016e4b248atagen2026-10-10 08:12 UTC404
        return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC405
            StatusCode::UNSUPPORTED_MEDIA_TYPE,
a9b016e4b248atagen2026-10-10 08:12 UTC406
            "patch-format",
a9b016e4b248atagen2026-10-10 08:12 UTC407
            "Patches are accepted as application/json-patch+json.",
a9b016e4b248atagen2026-10-10 08:12 UTC408
        )
a9b016e4b248atagen2026-10-10 08:12 UTC409
        .header(
a9b016e4b248atagen2026-10-10 08:12 UTC410
            HeaderName::from_static("accept-patch"),
a9b016e4b248atagen2026-10-10 08:12 UTC411
            "application/json-patch+json",
a9b016e4b248atagen2026-10-10 08:12 UTC412
        )
a9b016e4b248atagen2026-10-10 08:12 UTC413
        .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC414
    }
a9b016e4b248atagen2026-10-10 08:12 UTC415
    match serde_json::from_slice::<Vec<PatchOp>>(body) {
a9b016e4b248atagen2026-10-10 08:12 UTC416
        Ok(ops) => respond(run.patch(&ops)),
a9b016e4b248atagen2026-10-10 08:12 UTC417
        Err(_) => Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC418
            StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC419
            "malformed-patch",
a9b016e4b248atagen2026-10-10 08:12 UTC420
            "The patch could not be parsed.",
a9b016e4b248atagen2026-10-10 08:12 UTC421
        )
a9b016e4b248atagen2026-10-10 08:12 UTC422
        .into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC423
    }
a9b016e4b248atagen2026-10-10 08:12 UTC424
}
a9b016e4b248atagen2026-10-10 08:12 UTC425
a9b016e4b248atagen2026-10-10 08:12 UTC426
async fn length(
a9b016e4b248atagen2026-10-10 08:12 UTC427
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC428
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC429
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC430
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC431
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC432
    const ALLOW: &str = "GET, HEAD, OPTIONS";
a9b016e4b248atagen2026-10-10 08:12 UTC433
    with_run(&state, &id, &headers, true, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC434
        "GET" | "HEAD" => {
a9b016e4b248atagen2026-10-10 08:12 UTC435
            let content_type = [(header::CONTENT_TYPE, "text/plain; charset=us-ascii")];
a9b016e4b248atagen2026-10-10 08:12 UTC436
            if run.ruler {
a9b016e4b248atagen2026-10-10 08:12 UTC437
                let mm = (run.length * MM_PER_INCH).round() as usize;
a9b016e4b248atagen2026-10-10 08:12 UTC438
                return (StatusCode::OK, content_type, shaft(mm)).into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC439
            }
a9b016e4b248atagen2026-10-10 08:12 UTC440
            let mm = (run.self_reported() * MM_PER_INCH).round() as usize;
a9b016e4b248atagen2026-10-10 08:12 UTC441
            let chunks: Vec<Result<Bytes, Infallible>> = shaft(mm)
a9b016e4b248atagen2026-10-10 08:12 UTC442
                .into_bytes()
a9b016e4b248atagen2026-10-10 08:12 UTC443
                .chunks(16)
a9b016e4b248atagen2026-10-10 08:12 UTC444
                .map(|c| Ok(Bytes::copy_from_slice(c)))
a9b016e4b248atagen2026-10-10 08:12 UTC445
                .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC446
            let body = Body::from_stream(futures_util::stream::iter(chunks));
a9b016e4b248atagen2026-10-10 08:12 UTC447
            (StatusCode::OK, content_type, body).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC448
        }
a9b016e4b248atagen2026-10-10 08:12 UTC449
        "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 UTC450
        _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC451
    })
a9b016e4b248atagen2026-10-10 08:12 UTC452
}
a9b016e4b248atagen2026-10-10 08:12 UTC453
a9b016e4b248atagen2026-10-10 08:12 UTC454
async fn foreskin(
a9b016e4b248atagen2026-10-10 08:12 UTC455
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC456
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC457
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC458
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC459
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC460
    const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS";
a9b016e4b248atagen2026-10-10 08:12 UTC461
    with_run(&state, &id, &headers, true, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC462
        "GET" | "HEAD" => match run.foreskin {
a9b016e4b248atagen2026-10-10 08:12 UTC463
            Some(f) => (
a9b016e4b248atagen2026-10-10 08:12 UTC464
                StatusCode::OK,
a9b016e4b248atagen2026-10-10 08:12 UTC465
                [(header::ETAG, run.etag())],
a9b016e4b248atagen2026-10-10 08:12 UTC466
                Json(json!({ "foreskin": (f * 100.0).round() / 100.0 })),
a9b016e4b248atagen2026-10-10 08:12 UTC467
            )
a9b016e4b248atagen2026-10-10 08:12 UTC468
                .into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC469
            None => Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC470
                StatusCode::GONE,
a9b016e4b248atagen2026-10-10 08:12 UTC471
                "foreskin-gone",
a9b016e4b248atagen2026-10-10 08:12 UTC472
                "The foreskin is gone. It will not be returning.",
a9b016e4b248atagen2026-10-10 08:12 UTC473
            )
a9b016e4b248atagen2026-10-10 08:12 UTC474
            .into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC475
        },
a9b016e4b248atagen2026-10-10 08:12 UTC476
        "DELETE" => respond(run.delete_foreskin(header_str(&headers, header::IF_MATCH))),
a9b016e4b248atagen2026-10-10 08:12 UTC477
        "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 UTC478
        _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC479
    })
a9b016e4b248atagen2026-10-10 08:12 UTC480
}
a9b016e4b248atagen2026-10-10 08:12 UTC481
a9b016e4b248atagen2026-10-10 08:12 UTC482
fn items(run: &Run) -> Vec<(&'static str, &'static str)> {
a9b016e4b248atagen2026-10-10 08:12 UTC483
    let mut items = vec![(
a9b016e4b248atagen2026-10-10 08:12 UTC484
        "warm-towel",
a9b016e4b248atagen2026-10-10 08:12 UTC485
        "Reusable. Satisfies the warm-up precondition. Use technique=warmup.",
a9b016e4b248atagen2026-10-10 08:12 UTC486
    )];
a9b016e4b248atagen2026-10-10 08:12 UTC487
    if run.ice_packs > 0 {
a9b016e4b248atagen2026-10-10 08:12 UTC488
        items.push((
a9b016e4b248atagen2026-10-10 08:12 UTC489
            "ice-pack",
a9b016e4b248atagen2026-10-10 08:12 UTC490
            "Lowers EQ by 40. Use technique=ice. There is one.",
a9b016e4b248atagen2026-10-10 08:12 UTC491
        ));
a9b016e4b248atagen2026-10-10 08:12 UTC492
    }
a9b016e4b248atagen2026-10-10 08:12 UTC493
    if run.ruler {
a9b016e4b248atagen2026-10-10 08:12 UTC494
        items.push((
a9b016e4b248atagen2026-10-10 08:12 UTC495
            "ruler",
a9b016e4b248atagen2026-10-10 08:12 UTC496
            "Measures honestly. The length resource now carries a Content-Length.",
a9b016e4b248atagen2026-10-10 08:12 UTC497
        ));
a9b016e4b248atagen2026-10-10 08:12 UTC498
    }
a9b016e4b248atagen2026-10-10 08:12 UTC499
    if run.teapot {
a9b016e4b248atagen2026-10-10 08:12 UTC500
        items.push(("teapot-spout", "Does nothing. Cannot be dropped."));
a9b016e4b248atagen2026-10-10 08:12 UTC501
    }
a9b016e4b248atagen2026-10-10 08:12 UTC502
    items
a9b016e4b248atagen2026-10-10 08:12 UTC503
}
a9b016e4b248atagen2026-10-10 08:12 UTC504
a9b016e4b248atagen2026-10-10 08:12 UTC505
async fn inventory(
a9b016e4b248atagen2026-10-10 08:12 UTC506
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC507
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC508
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC509
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC510
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC511
    const ALLOW: &str = "GET, HEAD, OPTIONS";
a9b016e4b248atagen2026-10-10 08:12 UTC512
    with_run(&state, &id, &headers, false, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC513
        "GET" | "HEAD" => {
a9b016e4b248atagen2026-10-10 08:12 UTC514
            let listed: Vec<Value> = items(run)
a9b016e4b248atagen2026-10-10 08:12 UTC515
                .into_iter()
a9b016e4b248atagen2026-10-10 08:12 UTC516
                .map(|(name, _)| json!({ "name": name, "href": format!("/runs/{}/inventory/{name}", run.id) }))
a9b016e4b248atagen2026-10-10 08:12 UTC517
                .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC518
            Json(json!({ "items": listed, "excuses": EXCUSES })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC519
        }
a9b016e4b248atagen2026-10-10 08:12 UTC520
        "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 UTC521
        _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC522
    })
a9b016e4b248atagen2026-10-10 08:12 UTC523
}
a9b016e4b248atagen2026-10-10 08:12 UTC524
a9b016e4b248atagen2026-10-10 08:12 UTC525
async fn item(
a9b016e4b248atagen2026-10-10 08:12 UTC526
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC527
    Path((id, name)): Path<(String, String)>,
a9b016e4b248atagen2026-10-10 08:12 UTC528
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC529
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC530
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC531
    const ALLOW: &str = "GET, HEAD, DELETE, OPTIONS";
a9b016e4b248atagen2026-10-10 08:12 UTC532
    with_run(&state, &id, &headers, false, |run| {
a9b016e4b248atagen2026-10-10 08:12 UTC533
        let Some((name, description)) = items(run).into_iter().find(|(n, _)| *n == name) else {
a9b016e4b248atagen2026-10-10 08:12 UTC534
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC535
                StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC536
                "no-such-item",
a9b016e4b248atagen2026-10-10 08:12 UTC537
                "You do not have that.",
a9b016e4b248atagen2026-10-10 08:12 UTC538
            )
a9b016e4b248atagen2026-10-10 08:12 UTC539
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC540
        };
a9b016e4b248atagen2026-10-10 08:12 UTC541
        match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC542
            "GET" | "HEAD" => {
a9b016e4b248atagen2026-10-10 08:12 UTC543
                Json(json!({ "name": name, "description": description })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC544
            }
a9b016e4b248atagen2026-10-10 08:12 UTC545
            "DELETE" => Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC546
                StatusCode::FORBIDDEN,
a9b016e4b248atagen2026-10-10 08:12 UTC547
                "cannot-be-dropped",
a9b016e4b248atagen2026-10-10 08:12 UTC548
                "Items cannot be dropped.",
a9b016e4b248atagen2026-10-10 08:12 UTC549
            )
a9b016e4b248atagen2026-10-10 08:12 UTC550
            .into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC551
            "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 UTC552
            _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC553
        }
a9b016e4b248atagen2026-10-10 08:12 UTC554
    })
a9b016e4b248atagen2026-10-10 08:12 UTC555
}
a9b016e4b248atagen2026-10-10 08:12 UTC556
a9b016e4b248atagen2026-10-10 08:12 UTC557
async fn techniques(
a9b016e4b248atagen2026-10-10 08:12 UTC558
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC559
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC560
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC561
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC562
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC563
    const ALLOW: &str = "GET, HEAD, OPTIONS";
a9b016e4b248atagen2026-10-10 08:12 UTC564
    with_run(&state, &id, &headers, true, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC565
        "GET" | "HEAD" => {
a9b016e4b248atagen2026-10-10 08:12 UTC566
            let listed: Vec<Value> = Technique::ALL
a9b016e4b248atagen2026-10-10 08:12 UTC567
                .into_iter()
a9b016e4b248atagen2026-10-10 08:12 UTC568
                .map(|t| t.describe(run.forum_read))
a9b016e4b248atagen2026-10-10 08:12 UTC569
                .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC570
            Json(json!({
a9b016e4b248atagen2026-10-10 08:12 UTC571
                "usage": format!("JELQ /runs/{}?technique=<name>&intensity=<1-10>", run.id),
a9b016e4b248atagen2026-10-10 08:12 UTC572
                "lubricant": "Declared with Content-Type.",
a9b016e4b248atagen2026-10-10 08:12 UTC573
                "lubricants": LUBRICANTS,
a9b016e4b248atagen2026-10-10 08:12 UTC574
                "eq_band": [50, 70],
a9b016e4b248atagen2026-10-10 08:12 UTC575
                "techniques": listed,
a9b016e4b248atagen2026-10-10 08:12 UTC576
            }))
a9b016e4b248atagen2026-10-10 08:12 UTC577
            .into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC578
        }
a9b016e4b248atagen2026-10-10 08:12 UTC579
        "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 UTC580
        _ => unsupported(&method, ALLOW),
a9b016e4b248atagen2026-10-10 08:12 UTC581
    })
a9b016e4b248atagen2026-10-10 08:12 UTC582
}
a9b016e4b248atagen2026-10-10 08:12 UTC583
a9b016e4b248atagen2026-10-10 08:12 UTC584
async fn drawer(
a9b016e4b248atagen2026-10-10 08:12 UTC585
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC586
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC587
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC588
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC589
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC590
    with_run(&state, &id, &headers, true, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC591
        "POST" => run.search_drawer().into_response(),
a9b016e4b248atagen2026-10-10 08:12 UTC592
        "OPTIONS" => options("POST, OPTIONS", false),
a9b016e4b248atagen2026-10-10 08:12 UTC593
        _ => unsupported(&method, "POST, OPTIONS"),
a9b016e4b248atagen2026-10-10 08:12 UTC594
    })
a9b016e4b248atagen2026-10-10 08:12 UTC595
}
a9b016e4b248atagen2026-10-10 08:12 UTC596
a9b016e4b248atagen2026-10-10 08:12 UTC597
async fn branch(
a9b016e4b248atagen2026-10-10 08:12 UTC598
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC599
    Path((id, choice)): Path<(String, String)>,
a9b016e4b248atagen2026-10-10 08:12 UTC600
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC601
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC602
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC603
    with_run(&state, &id, &headers, true, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC604
        "POST" => respond(run.choose(&choice)),
a9b016e4b248atagen2026-10-10 08:12 UTC605
        "OPTIONS" => options("POST, OPTIONS", false),
a9b016e4b248atagen2026-10-10 08:12 UTC606
        _ => unsupported(&method, "POST, OPTIONS"),
a9b016e4b248atagen2026-10-10 08:12 UTC607
    })
a9b016e4b248atagen2026-10-10 08:12 UTC608
}
a9b016e4b248atagen2026-10-10 08:12 UTC609
a9b016e4b248atagen2026-10-10 08:12 UTC610
async fn diagnosis(
a9b016e4b248atagen2026-10-10 08:12 UTC611
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC612
    Path(id): Path<String>,
a9b016e4b248atagen2026-10-10 08:12 UTC613
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC614
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC615
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC616
    const ALLOW: &str = "GET, HEAD, OPTIONS";
a9b016e4b248atagen2026-10-10 08:12 UTC617
    with_run(&state, &id, &headers, false, |run| match method.as_str() {
a9b016e4b248atagen2026-10-10 08:12 UTC618
        "GET" | "HEAD" => respond(run.diagnose(
a9b016e4b248atagen2026-10-10 08:12 UTC619
            header_str(&headers, header::AUTHORIZATION),
a9b016e4b248atagen2026-10-10 08:12 UTC620
            header_str(&headers, header::IF_NONE_MATCH),
a9b016e4b248atagen2026-10-10 08:12 UTC621
        )),
a9b016e4b248atagen2026-10-10 08:12 UTC622
        "OPTIONS" => options(ALLOW, false),
a9b016e4b248atagen2026-10-10 08:12 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
async fn graveyard(State(state): State<Shared>, method: Method) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC628
    if method != Method::GET && method != Method::HEAD {
a9b016e4b248atagen2026-10-10 08:12 UTC629
        return unsupported(&method, "GET, HEAD");
a9b016e4b248atagen2026-10-10 08:12 UTC630
    }
a9b016e4b248atagen2026-10-10 08:12 UTC631
    let runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC632
    let dead: Vec<Value> = runs
a9b016e4b248atagen2026-10-10 08:12 UTC633
        .values()
a9b016e4b248atagen2026-10-10 08:12 UTC634
        .filter_map(|run| {
a9b016e4b248atagen2026-10-10 08:12 UTC635
            let ending = run.ended.as_ref()?;
a9b016e4b248atagen2026-10-10 08:12 UTC636
            Some(json!({
a9b016e4b248atagen2026-10-10 08:12 UTC637
                "id": run.id,
a9b016e4b248atagen2026-10-10 08:12 UTC638
                "cause": format!("{PROBLEM_BASE}{}", ending.cause),
a9b016e4b248atagen2026-10-10 08:12 UTC639
                "detail": ending.detail,
a9b016e4b248atagen2026-10-10 08:12 UTC640
                "turn": ending.turn,
a9b016e4b248atagen2026-10-10 08:12 UTC641
            }))
a9b016e4b248atagen2026-10-10 08:12 UTC642
        })
a9b016e4b248atagen2026-10-10 08:12 UTC643
        .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC644
    Json(json!({ "runs": dead })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC645
}
a9b016e4b248atagen2026-10-10 08:12 UTC646
a9b016e4b248atagen2026-10-10 08:12 UTC647
async fn leaderboard(
a9b016e4b248atagen2026-10-10 08:12 UTC648
    State(state): State<Shared>,
a9b016e4b248atagen2026-10-10 08:12 UTC649
    method: Method,
a9b016e4b248atagen2026-10-10 08:12 UTC650
    Query(query): Query<HashMap<String, String>>,
a9b016e4b248atagen2026-10-10 08:12 UTC651
    headers: HeaderMap,
a9b016e4b248atagen2026-10-10 08:12 UTC652
) -> Response {
a9b016e4b248atagen2026-10-10 08:12 UTC653
    if method != Method::GET && method != Method::HEAD {
a9b016e4b248atagen2026-10-10 08:12 UTC654
        return unsupported(&method, "GET, HEAD");
a9b016e4b248atagen2026-10-10 08:12 UTC655
    }
a9b016e4b248atagen2026-10-10 08:12 UTC656
    let truthful = match query.get("sort").map(String::as_str) {
a9b016e4b248atagen2026-10-10 08:12 UTC657
        None | Some("self-reported-length") => false,
a9b016e4b248atagen2026-10-10 08:12 UTC658
        Some("content-length") if cookie(&headers, "ruler") == Some("1") => true,
a9b016e4b248atagen2026-10-10 08:12 UTC659
        Some("content-length") => {
a9b016e4b248atagen2026-10-10 08:12 UTC660
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC661
                StatusCode::FORBIDDEN,
a9b016e4b248atagen2026-10-10 08:12 UTC662
                "ruler-required",
a9b016e4b248atagen2026-10-10 08:12 UTC663
                "Sorting by the truth requires a Ruler.",
a9b016e4b248atagen2026-10-10 08:12 UTC664
            )
a9b016e4b248atagen2026-10-10 08:12 UTC665
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC666
        }
a9b016e4b248atagen2026-10-10 08:12 UTC667
        Some(_) => {
a9b016e4b248atagen2026-10-10 08:12 UTC668
            return Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC669
                StatusCode::BAD_REQUEST,
a9b016e4b248atagen2026-10-10 08:12 UTC670
                "unknown-sort",
a9b016e4b248atagen2026-10-10 08:12 UTC671
                "The leaderboard sorts by self-reported-length or content-length.",
a9b016e4b248atagen2026-10-10 08:12 UTC672
            )
a9b016e4b248atagen2026-10-10 08:12 UTC673
            .into_response();
a9b016e4b248atagen2026-10-10 08:12 UTC674
        }
a9b016e4b248atagen2026-10-10 08:12 UTC675
    };
a9b016e4b248atagen2026-10-10 08:12 UTC676
    let runs = state.runs.lock().unwrap();
a9b016e4b248atagen2026-10-10 08:12 UTC677
    let mut entries: Vec<&Run> = runs.values().collect();
a9b016e4b248atagen2026-10-10 08:12 UTC678
    entries.sort_by(|a, b| {
a9b016e4b248atagen2026-10-10 08:12 UTC679
        let key = |r: &Run| {
a9b016e4b248atagen2026-10-10 08:12 UTC680
            if truthful {
a9b016e4b248atagen2026-10-10 08:12 UTC681
                r.length
a9b016e4b248atagen2026-10-10 08:12 UTC682
            } else {
a9b016e4b248atagen2026-10-10 08:12 UTC683
                r.self_reported()
a9b016e4b248atagen2026-10-10 08:12 UTC684
            }
a9b016e4b248atagen2026-10-10 08:12 UTC685
        };
a9b016e4b248atagen2026-10-10 08:12 UTC686
        key(b).total_cmp(&key(a))
a9b016e4b248atagen2026-10-10 08:12 UTC687
    });
a9b016e4b248atagen2026-10-10 08:12 UTC688
    let entries: Vec<Value> = entries
a9b016e4b248atagen2026-10-10 08:12 UTC689
        .into_iter()
a9b016e4b248atagen2026-10-10 08:12 UTC690
        .map(|run| {
a9b016e4b248atagen2026-10-10 08:12 UTC691
            let mut entry = json!({
a9b016e4b248atagen2026-10-10 08:12 UTC692
                "id": run.id,
a9b016e4b248atagen2026-10-10 08:12 UTC693
                "status": run.status(),
a9b016e4b248atagen2026-10-10 08:12 UTC694
                "self_reported_length": (run.self_reported() * 100.0).round() / 100.0,
a9b016e4b248atagen2026-10-10 08:12 UTC695
            });
a9b016e4b248atagen2026-10-10 08:12 UTC696
            if truthful {
a9b016e4b248atagen2026-10-10 08:12 UTC697
                entry["content_length"] = json!((run.length * MM_PER_INCH).round() as u64);
a9b016e4b248atagen2026-10-10 08:12 UTC698
            }
a9b016e4b248atagen2026-10-10 08:12 UTC699
            entry
a9b016e4b248atagen2026-10-10 08:12 UTC700
        })
a9b016e4b248atagen2026-10-10 08:12 UTC701
        .collect();
a9b016e4b248atagen2026-10-10 08:12 UTC702
    Json(json!({ "entries": entries })).into_response()
a9b016e4b248atagen2026-10-10 08:12 UTC703
}
a9b016e4b248atagen2026-10-10 08:12 UTC704
a9b016e4b248atagen2026-10-10 08:12 UTC705
async fn not_found() -> Problem {
a9b016e4b248atagen2026-10-10 08:12 UTC706
    Problem::new(
a9b016e4b248atagen2026-10-10 08:12 UTC707
        StatusCode::NOT_FOUND,
a9b016e4b248atagen2026-10-10 08:12 UTC708
        "not-found",
a9b016e4b248atagen2026-10-10 08:12 UTC709
        "The resource does not exist. This is not about you.",
a9b016e4b248atagen2026-10-10 08:12 UTC710
    )
a9b016e4b248atagen2026-10-10 08:12 UTC711
}