atagen

jelq

public

an http based roguelike rpg about jelqing

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