atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/http/runs.rs

782069860236atagen2026-10-10 12:44 UTC1
//! The resources under `/runs/{id}`.
782069860236atagen2026-10-10 12:44 UTC2
782069860236atagen2026-10-10 12:44 UTC3
use std::{convert::Infallible, fmt::Write as _};
782069860236atagen2026-10-10 12:44 UTC4
782069860236atagen2026-10-10 12:44 UTC5
use axum::{
782069860236atagen2026-10-10 12:44 UTC6
    Json,
782069860236atagen2026-10-10 12:44 UTC7
    body::{Body, Bytes},
782069860236atagen2026-10-10 12:44 UTC8
    http::{StatusCode, header},
782069860236atagen2026-10-10 12:44 UTC9
    response::{IntoResponse, Response},
782069860236atagen2026-10-10 12:44 UTC10
};
782069860236atagen2026-10-10 12:44 UTC11
use serde_json::{Value, json};
782069860236atagen2026-10-10 12:44 UTC12
782069860236atagen2026-10-10 12:44 UTC13
use super::{
782069860236atagen2026-10-10 12:44 UTC14
    ctx::Ctx,
782069860236atagen2026-10-10 12:44 UTC15
    resource::{Resource, Verb},
782069860236atagen2026-10-10 12:44 UTC16
    response::respond,
782069860236atagen2026-10-10 12:44 UTC17
    run::{Greeting, RunResource, Scope},
782069860236atagen2026-10-10 12:44 UTC18
};
782069860236atagen2026-10-10 12:44 UTC19
use crate::{
782069860236atagen2026-10-10 12:44 UTC20
    game::{
782069860236atagen2026-10-10 12:44 UTC21
        Cause, Floor, Grip, Item, LUBRICANTS, Lube, PatchOp, Run, Technique,
782069860236atagen2026-10-10 12:44 UTC22
        balance::{EQ_BAND, MM_PER_INCH},
782069860236atagen2026-10-10 12:44 UTC23
        shaft,
782069860236atagen2026-10-10 12:44 UTC24
    },
782069860236atagen2026-10-10 12:44 UTC25
    headers::ACCEPT_PATCH,
782069860236atagen2026-10-10 12:44 UTC26
    problem::Problem,
782069860236atagen2026-10-10 12:44 UTC27
};
782069860236atagen2026-10-10 12:44 UTC28
782069860236atagen2026-10-10 12:44 UTC29
const fn home(verbs: &'static [Verb<Run>]) -> RunResource {
782069860236atagen2026-10-10 12:44 UTC30
    RunResource {
782069860236atagen2026-10-10 12:44 UTC31
        scope: Scope::Home,
782069860236atagen2026-10-10 12:44 UTC32
        resource: Resource::new(verbs),
782069860236atagen2026-10-10 12:44 UTC33
    }
782069860236atagen2026-10-10 12:44 UTC34
}
782069860236atagen2026-10-10 12:44 UTC35
782069860236atagen2026-10-10 12:44 UTC36
const fn pockets(verbs: &'static [Verb<Run>]) -> RunResource {
782069860236atagen2026-10-10 12:44 UTC37
    RunResource {
782069860236atagen2026-10-10 12:44 UTC38
        scope: Scope::Pockets,
782069860236atagen2026-10-10 12:44 UTC39
        resource: Resource::new(verbs),
782069860236atagen2026-10-10 12:44 UTC40
    }
782069860236atagen2026-10-10 12:44 UTC41
}
782069860236atagen2026-10-10 12:44 UTC42
782069860236atagen2026-10-10 12:44 UTC43
fn not_in_clinic(run: &Run) -> bool {
782069860236atagen2026-10-10 12:44 UTC44
    run.floor != Floor::Clinic
782069860236atagen2026-10-10 12:44 UTC45
}
782069860236atagen2026-10-10 12:44 UTC46
782069860236atagen2026-10-10 12:44 UTC47
fn never(_: &Run) -> bool {
782069860236atagen2026-10-10 12:44 UTC48
    false
782069860236atagen2026-10-10 12:44 UTC49
}
782069860236atagen2026-10-10 12:44 UTC50
782069860236atagen2026-10-10 12:44 UTC51
fn unreachable_handler(_: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC52
    StatusCode::METHOD_NOT_ALLOWED.into_response()
782069860236atagen2026-10-10 12:44 UTC53
}
782069860236atagen2026-10-10 12:44 UTC54
782069860236atagen2026-10-10 12:44 UTC55
pub static RUN: RunResource = RunResource {
782069860236atagen2026-10-10 12:44 UTC56
    scope: Scope::Home,
782069860236atagen2026-10-10 12:44 UTC57
    resource: Resource {
782069860236atagen2026-10-10 12:44 UTC58
        verbs: &[
782069860236atagen2026-10-10 12:44 UTC59
            Verb::new("GET", get_run),
782069860236atagen2026-10-10 12:44 UTC60
            Verb::new("JELQ", jelq)
782069860236atagen2026-10-10 12:44 UTC61
                .only_when(not_in_clinic, "JELQ is not performed in the clinic."),
4347977b8192atagen2026-10-11 04:56 UTC62
            Verb::new("HOLD", hold_run).only_when(
4347977b8192atagen2026-10-11 04:56 UTC63
                Run::can_hold,
4347977b8192atagen2026-10-11 04:56 UTC64
                "HOLD is only available above 70 EQ or at 2 stamina or less.",
4347977b8192atagen2026-10-11 04:56 UTC65
            ),
782069860236atagen2026-10-10 12:44 UTC66
            Verb::new("PUT", put_run),
782069860236atagen2026-10-10 12:44 UTC67
            Verb::new("PATCH", patch_run),
782069860236atagen2026-10-10 12:44 UTC68
            Verb::new("DELETE", delete_run),
782069860236atagen2026-10-10 12:44 UTC69
            Verb::new("TRACE", trace_run),
782069860236atagen2026-10-10 12:44 UTC70
        ],
782069860236atagen2026-10-10 12:44 UTC71
        accept_patch: true,
782069860236atagen2026-10-10 12:44 UTC72
    },
782069860236atagen2026-10-10 12:44 UTC73
};
782069860236atagen2026-10-10 12:44 UTC74
pub static LENGTH: RunResource = home(&[Verb::new("GET", get_length)]);
782069860236atagen2026-10-10 12:44 UTC75
pub static FORESKIN: RunResource = home(&[
782069860236atagen2026-10-10 12:44 UTC76
    Verb::new("GET", get_foreskin),
782069860236atagen2026-10-10 12:44 UTC77
    Verb::new("DELETE", delete_foreskin),
782069860236atagen2026-10-10 12:44 UTC78
]);
782069860236atagen2026-10-10 12:44 UTC79
pub static INVENTORY: RunResource = pockets(&[Verb::new("GET", get_inventory)]);
782069860236atagen2026-10-10 12:44 UTC80
pub static ITEM: RunResource =
782069860236atagen2026-10-10 12:44 UTC81
    pockets(&[Verb::new("GET", get_item), Verb::new("DELETE", delete_item)]);
782069860236atagen2026-10-10 12:44 UTC82
pub static TECHNIQUES: RunResource = home(&[Verb::new("GET", get_techniques)]);
782069860236atagen2026-10-10 12:44 UTC83
pub static DRAWER: RunResource = home(&[Verb::new("POST", search_drawer)]);
782069860236atagen2026-10-10 12:44 UTC84
pub static BRANCH: RunResource = home(&[Verb::new("POST", choose_branch)]);
0a0bfee8329eatagen2026-10-11 05:11 UTC85
pub static KITCHEN: RunResource = home(&[Verb::new("POST", raid_kitchen)]);
0a0bfee8329eatagen2026-10-11 05:11 UTC86
pub static MELON: RunResource = home(&[Verb::new("POST", commit_melon)]);
b85a0d4536c3atagen2026-10-11 08:50 UTC87
pub static REREAD: RunResource = pockets(&[Verb::new("POST", re_read)]);
782069860236atagen2026-10-10 12:44 UTC88
pub static FLOOR: RunResource =
0d604ee39751atagen2026-10-10 14:15 UTC89
    home(&[Verb::new("GET", get_floor), Verb::new("POST", leave_floor)]);
782069860236atagen2026-10-10 12:44 UTC90
pub static SHOP: RunResource = home(&[Verb::new("GET", get_shop)]);
782069860236atagen2026-10-10 12:44 UTC91
pub static BUY: RunResource = home(&[Verb::new("POST", buy)]);
782069860236atagen2026-10-10 12:44 UTC92
pub static PORTAL: RunResource = pockets(&[
782069860236atagen2026-10-10 12:44 UTC93
    Verb::new("GET", get_portal),
782069860236atagen2026-10-10 12:44 UTC94
    Verb::new("POST", accept_portal),
782069860236atagen2026-10-10 12:44 UTC95
]);
782069860236atagen2026-10-10 12:44 UTC96
pub static ELDER: RunResource = home(&[Verb::new("GET", get_elder)]);
782069860236atagen2026-10-10 12:44 UTC97
pub static GRIP: RunResource = home(&[Verb::new("GET", get_grip)]);
782069860236atagen2026-10-10 12:44 UTC98
pub static DIAGNOSIS: RunResource = pockets(&[Verb::new("GET", get_diagnosis)]);
782069860236atagen2026-10-10 12:44 UTC99
pub static FOOTAGE: RunResource = pockets(&[
782069860236atagen2026-10-10 12:44 UTC100
    Verb::new("GET", get_footage),
782069860236atagen2026-10-10 12:44 UTC101
    Verb::new("DELETE", unreachable_handler).only_when(
782069860236atagen2026-10-10 12:44 UTC102
        never,
782069860236atagen2026-10-10 12:44 UTC103
        "The footage has been cached by intermediaries. It cannot be deleted.",
782069860236atagen2026-10-10 12:44 UTC104
    ),
782069860236atagen2026-10-10 12:44 UTC105
]);
782069860236atagen2026-10-10 12:44 UTC106
782069860236atagen2026-10-10 12:44 UTC107
fn get_run(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC108
    if ctx
782069860236atagen2026-10-10 12:44 UTC109
        .header(header::ACCEPT)
782069860236atagen2026-10-10 12:44 UTC110
        .is_some_and(|accept| accept.contains("reassurance"))
782069860236atagen2026-10-10 12:44 UTC111
    {
782069860236atagen2026-10-10 12:44 UTC112
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC113
            StatusCode::NOT_ACCEPTABLE,
782069860236atagen2026-10-10 12:44 UTC114
            "no-reassurance",
782069860236atagen2026-10-10 12:44 UTC115
            "The server does not produce reassurance.",
782069860236atagen2026-10-10 12:44 UTC116
        )
782069860236atagen2026-10-10 12:44 UTC117
        .into_response();
782069860236atagen2026-10-10 12:44 UTC118
    }
782069860236atagen2026-10-10 12:44 UTC119
    (
782069860236atagen2026-10-10 12:44 UTC120
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC121
        [(header::ETAG, run.etag())],
782069860236atagen2026-10-10 12:44 UTC122
        Json(run.view()),
782069860236atagen2026-10-10 12:44 UTC123
    )
782069860236atagen2026-10-10 12:44 UTC124
        .into_response()
782069860236atagen2026-10-10 12:44 UTC125
}
782069860236atagen2026-10-10 12:44 UTC126
782069860236atagen2026-10-10 12:44 UTC127
fn parse_techniques(run: &Run, ctx: &Ctx) -> Result<Vec<Technique>, Problem> {
782069860236atagen2026-10-10 12:44 UTC128
    let required = || {
782069860236atagen2026-10-10 12:44 UTC129
        Problem::new(
782069860236atagen2026-10-10 12:44 UTC130
            StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC131
            "technique-required",
782069860236atagen2026-10-10 12:44 UTC132
            "No technique was specified.",
782069860236atagen2026-10-10 12:44 UTC133
        )
782069860236atagen2026-10-10 12:44 UTC134
        .with("techniques", run.path("/techniques"))
782069860236atagen2026-10-10 12:44 UTC135
    };
782069860236atagen2026-10-10 12:44 UTC136
    let names = ctx
782069860236atagen2026-10-10 12:44 UTC137
        .query("technique")
782069860236atagen2026-10-10 12:44 UTC138
        .filter(|n| !n.trim().is_empty())
782069860236atagen2026-10-10 12:44 UTC139
        .ok_or_else(required)?;
782069860236atagen2026-10-10 12:44 UTC140
    names
782069860236atagen2026-10-10 12:44 UTC141
        .split(',')
782069860236atagen2026-10-10 12:44 UTC142
        .map(str::trim)
782069860236atagen2026-10-10 12:44 UTC143
        .filter(|n| !n.is_empty())
782069860236atagen2026-10-10 12:44 UTC144
        .map(|name| {
782069860236atagen2026-10-10 12:44 UTC145
            Technique::parse(name).ok_or_else(|| {
782069860236atagen2026-10-10 12:44 UTC146
                Problem::new(
782069860236atagen2026-10-10 12:44 UTC147
                    StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC148
                    "unknown-technique",
782069860236atagen2026-10-10 12:44 UTC149
                    format!("{name} is not a technique."),
782069860236atagen2026-10-10 12:44 UTC150
                )
782069860236atagen2026-10-10 12:44 UTC151
                .with("techniques", run.path("/techniques"))
782069860236atagen2026-10-10 12:44 UTC152
            })
782069860236atagen2026-10-10 12:44 UTC153
        })
782069860236atagen2026-10-10 12:44 UTC154
        .collect()
782069860236atagen2026-10-10 12:44 UTC155
}
782069860236atagen2026-10-10 12:44 UTC156
782069860236atagen2026-10-10 12:44 UTC157
fn parse_intensity(ctx: &Ctx) -> Result<i32, Problem> {
782069860236atagen2026-10-10 12:44 UTC158
    match ctx.query("intensity").map(str::parse::<i32>) {
782069860236atagen2026-10-10 12:44 UTC159
        None => Ok(5),
782069860236atagen2026-10-10 12:44 UTC160
        Some(Ok(i)) if (1..=10).contains(&i) => Ok(i),
782069860236atagen2026-10-10 12:44 UTC161
        Some(_) => Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC162
            StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC163
            "intensity-out-of-range",
782069860236atagen2026-10-10 12:44 UTC164
            "Intensity is measured from 1 to 10.",
782069860236atagen2026-10-10 12:44 UTC165
        )),
782069860236atagen2026-10-10 12:44 UTC166
    }
782069860236atagen2026-10-10 12:44 UTC167
}
782069860236atagen2026-10-10 12:44 UTC168
782069860236atagen2026-10-10 12:44 UTC169
fn jelq(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC170
    let parsed = parse_techniques(run, ctx).and_then(|techniques| {
782069860236atagen2026-10-10 12:44 UTC171
        let intensity = parse_intensity(ctx)?;
782069860236atagen2026-10-10 12:44 UTC172
        let lube = Lube::from_content_type(ctx.header(header::CONTENT_TYPE))?;
782069860236atagen2026-10-10 12:44 UTC173
        Ok((techniques, intensity, lube))
782069860236atagen2026-10-10 12:44 UTC174
    });
782069860236atagen2026-10-10 12:44 UTC175
    match parsed {
a6caa2f99786atagen2026-10-12 10:38 UTC176
        Ok((techniques, intensity, lube)) => respond(run.jelq(&techniques, intensity, lube)),
782069860236atagen2026-10-10 12:44 UTC177
        Err(problem) => problem.into_response(),
782069860236atagen2026-10-10 12:44 UTC178
    }
782069860236atagen2026-10-10 12:44 UTC179
}
782069860236atagen2026-10-10 12:44 UTC180
782069860236atagen2026-10-10 12:44 UTC181
fn put_run(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC182
    run.put().into_response()
782069860236atagen2026-10-10 12:44 UTC183
}
782069860236atagen2026-10-10 12:44 UTC184
a6caa2f99786atagen2026-10-12 10:38 UTC185
fn hold_run(run: &mut Run, _: &Ctx) -> Response {
a6caa2f99786atagen2026-10-12 10:38 UTC186
    respond(run.hold())
4347977b8192atagen2026-10-11 04:56 UTC187
}
4347977b8192atagen2026-10-11 04:56 UTC188
782069860236atagen2026-10-10 12:44 UTC189
fn patch_run(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC190
    let is_patch = ctx
782069860236atagen2026-10-10 12:44 UTC191
        .header(header::CONTENT_TYPE)
782069860236atagen2026-10-10 12:44 UTC192
        .is_some_and(|ct| ct.starts_with("application/json-patch+json"));
782069860236atagen2026-10-10 12:44 UTC193
    if !is_patch {
782069860236atagen2026-10-10 12:44 UTC194
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC195
            StatusCode::UNSUPPORTED_MEDIA_TYPE,
782069860236atagen2026-10-10 12:44 UTC196
            "patch-format",
782069860236atagen2026-10-10 12:44 UTC197
            "Patches are accepted as application/json-patch+json.",
782069860236atagen2026-10-10 12:44 UTC198
        )
782069860236atagen2026-10-10 12:44 UTC199
        .header(ACCEPT_PATCH, "application/json-patch+json")
782069860236atagen2026-10-10 12:44 UTC200
        .into_response();
782069860236atagen2026-10-10 12:44 UTC201
    }
782069860236atagen2026-10-10 12:44 UTC202
    match serde_json::from_slice::<Vec<PatchOp>>(&ctx.body) {
782069860236atagen2026-10-10 12:44 UTC203
        Ok(ops) => respond(run.patch(&ops)),
782069860236atagen2026-10-10 12:44 UTC204
        Err(_) => Problem::new(
782069860236atagen2026-10-10 12:44 UTC205
            StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC206
            "malformed-patch",
782069860236atagen2026-10-10 12:44 UTC207
            "The patch could not be parsed.",
782069860236atagen2026-10-10 12:44 UTC208
        )
782069860236atagen2026-10-10 12:44 UTC209
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC210
    }
782069860236atagen2026-10-10 12:44 UTC211
}
782069860236atagen2026-10-10 12:44 UTC212
782069860236atagen2026-10-10 12:44 UTC213
fn delete_run(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC214
    run.end(Cause::Abandoned);
782069860236atagen2026-10-10 12:44 UTC215
    StatusCode::NO_CONTENT.into_response()
782069860236atagen2026-10-10 12:44 UTC216
}
782069860236atagen2026-10-10 12:44 UTC217
782069860236atagen2026-10-10 12:44 UTC218
fn trace_run(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC219
    run.shame += 1;
782069860236atagen2026-10-10 12:44 UTC220
    let mut message = format!("{} {} {:?}\r\n", ctx.method, ctx.uri, ctx.version);
782069860236atagen2026-10-10 12:44 UTC221
    for (name, value) in &ctx.headers {
782069860236atagen2026-10-10 12:44 UTC222
        let _ = write!(
782069860236atagen2026-10-10 12:44 UTC223
            message,
782069860236atagen2026-10-10 12:44 UTC224
            "{name}: {}\r\n",
782069860236atagen2026-10-10 12:44 UTC225
            value.to_str().unwrap_or("<binary>")
782069860236atagen2026-10-10 12:44 UTC226
        );
782069860236atagen2026-10-10 12:44 UTC227
    }
782069860236atagen2026-10-10 12:44 UTC228
    message.push_str("\r\n");
782069860236atagen2026-10-10 12:44 UTC229
    (
782069860236atagen2026-10-10 12:44 UTC230
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC231
        [(header::CONTENT_TYPE, "message/http")],
782069860236atagen2026-10-10 12:44 UTC232
        message,
782069860236atagen2026-10-10 12:44 UTC233
    )
782069860236atagen2026-10-10 12:44 UTC234
        .into_response()
782069860236atagen2026-10-10 12:44 UTC235
}
782069860236atagen2026-10-10 12:44 UTC236
782069860236atagen2026-10-10 12:44 UTC237
fn millimetres(inches: f64) -> usize {
782069860236atagen2026-10-10 12:44 UTC238
    (inches * MM_PER_INCH).round() as usize
782069860236atagen2026-10-10 12:44 UTC239
}
782069860236atagen2026-10-10 12:44 UTC240
d46f9d0b953aatagen2026-10-10 14:41 UTC241
enum InchRange {
d46f9d0b953aatagen2026-10-10 14:41 UTC242
    Satisfiable(f64, f64),
d46f9d0b953aatagen2026-10-10 14:41 UTC243
    Unsatisfiable,
d46f9d0b953aatagen2026-10-10 14:41 UTC244
}
d46f9d0b953aatagen2026-10-10 14:41 UTC245
d46f9d0b953aatagen2026-10-10 14:41 UTC246
/// `None` means the header is not a valid range, which RFC 9110 §14.2 says to ignore rather than refuse.
d46f9d0b953aatagen2026-10-10 14:41 UTC247
fn parse_inch_range(range: &str, total: f64) -> Option<InchRange> {
782069860236atagen2026-10-10 12:44 UTC248
    let (start, end) = range.strip_prefix("inches=")?.split_once('-')?;
d46f9d0b953aatagen2026-10-10 14:41 UTC249
    let number = |text: &str| {
d46f9d0b953aatagen2026-10-10 14:41 UTC250
        text.parse::<f64>()
d46f9d0b953aatagen2026-10-10 14:41 UTC251
            .ok()
d46f9d0b953aatagen2026-10-10 14:41 UTC252
            .filter(|n| n.is_finite() && *n >= 0.0)
782069860236atagen2026-10-10 12:44 UTC253
    };
d46f9d0b953aatagen2026-10-10 14:41 UTC254
    let (start, end) = match (start.trim(), end.trim()) {
d46f9d0b953aatagen2026-10-10 14:41 UTC255
        ("", "") => return None,
d46f9d0b953aatagen2026-10-10 14:41 UTC256
        ("", suffix) => ((total - number(suffix)?).max(0.0), total),
d46f9d0b953aatagen2026-10-10 14:41 UTC257
        (start, "") => (number(start)?, total),
d46f9d0b953aatagen2026-10-10 14:41 UTC258
        (start, end) => {
d46f9d0b953aatagen2026-10-10 14:41 UTC259
            let (start, end) = (number(start)?, number(end)?);
d46f9d0b953aatagen2026-10-10 14:41 UTC260
            if start > end {
d46f9d0b953aatagen2026-10-10 14:41 UTC261
                return None;
d46f9d0b953aatagen2026-10-10 14:41 UTC262
            }
d46f9d0b953aatagen2026-10-10 14:41 UTC263
            (start, end)
d46f9d0b953aatagen2026-10-10 14:41 UTC264
        }
d46f9d0b953aatagen2026-10-10 14:41 UTC265
    };
d46f9d0b953aatagen2026-10-10 14:41 UTC266
    Some(if start < total {
d46f9d0b953aatagen2026-10-10 14:41 UTC267
        InchRange::Satisfiable(start, end.min(total))
d46f9d0b953aatagen2026-10-10 14:41 UTC268
    } else {
d46f9d0b953aatagen2026-10-10 14:41 UTC269
        InchRange::Unsatisfiable
d46f9d0b953aatagen2026-10-10 14:41 UTC270
    })
782069860236atagen2026-10-10 12:44 UTC271
}
782069860236atagen2026-10-10 12:44 UTC272
ec6fd4bb5b60atagen2026-10-11 04:34 UTC273
fn unsatisfied_range(inches: f64) -> Response {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC274
    Problem::new(
ec6fd4bb5b60atagen2026-10-11 04:34 UTC275
        StatusCode::RANGE_NOT_SATISFIABLE,
ec6fd4bb5b60atagen2026-10-11 04:34 UTC276
        "range-not-satisfiable",
ec6fd4bb5b60atagen2026-10-11 04:34 UTC277
        format!("The requested range is not available. {inches:.2} inches are."),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC278
    )
ec6fd4bb5b60atagen2026-10-11 04:34 UTC279
    .header(header::CONTENT_RANGE, format!("inches */{inches:.2}"))
ec6fd4bb5b60atagen2026-10-11 04:34 UTC280
    .into_response()
ec6fd4bb5b60atagen2026-10-11 04:34 UTC281
}
ec6fd4bb5b60atagen2026-10-11 04:34 UTC282
ec6fd4bb5b60atagen2026-10-11 04:34 UTC283
fn partial_inches(body: &str, offset: usize, start: f64, end: f64, total: f64) -> Response {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC284
    let from = (millimetres(start) + offset).min(body.len() - 1);
ec6fd4bb5b60atagen2026-10-11 04:34 UTC285
    let to = (millimetres(end) + offset).clamp(from + 1, body.len());
ec6fd4bb5b60atagen2026-10-11 04:34 UTC286
    (
ec6fd4bb5b60atagen2026-10-11 04:34 UTC287
        StatusCode::PARTIAL_CONTENT,
ec6fd4bb5b60atagen2026-10-11 04:34 UTC288
        [
ec6fd4bb5b60atagen2026-10-11 04:34 UTC289
            (
ec6fd4bb5b60atagen2026-10-11 04:34 UTC290
                header::CONTENT_TYPE,
ec6fd4bb5b60atagen2026-10-11 04:34 UTC291
                "text/plain; charset=us-ascii".to_owned(),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC292
            ),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC293
            (header::ACCEPT_RANGES, "inches".to_owned()),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC294
            (
ec6fd4bb5b60atagen2026-10-11 04:34 UTC295
                header::CONTENT_RANGE,
ec6fd4bb5b60atagen2026-10-11 04:34 UTC296
                format!("inches {start:.2}-{end:.2}/{total:.2}"),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC297
            ),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC298
        ],
ec6fd4bb5b60atagen2026-10-11 04:34 UTC299
        body[from..to].to_owned(),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC300
    )
ec6fd4bb5b60atagen2026-10-11 04:34 UTC301
        .into_response()
ec6fd4bb5b60atagen2026-10-11 04:34 UTC302
}
ec6fd4bb5b60atagen2026-10-11 04:34 UTC303
782069860236atagen2026-10-10 12:44 UTC304
fn get_length(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC305
    let inches = if run.items.ruler {
782069860236atagen2026-10-10 12:44 UTC306
        run.current_length()
782069860236atagen2026-10-10 12:44 UTC307
    } else {
782069860236atagen2026-10-10 12:44 UTC308
        run.self_reported()
782069860236atagen2026-10-10 12:44 UTC309
    };
782069860236atagen2026-10-10 12:44 UTC310
    let body = shaft(millimetres(inches));
782069860236atagen2026-10-10 12:44 UTC311
    let content_type = (
782069860236atagen2026-10-10 12:44 UTC312
        header::CONTENT_TYPE,
782069860236atagen2026-10-10 12:44 UTC313
        "text/plain; charset=us-ascii".to_owned(),
782069860236atagen2026-10-10 12:44 UTC314
    );
782069860236atagen2026-10-10 12:44 UTC315
    let accept_ranges = (header::ACCEPT_RANGES, "inches".to_owned());
782069860236atagen2026-10-10 12:44 UTC316
d46f9d0b953aatagen2026-10-10 14:41 UTC317
    if let Some(range) = ctx
d46f9d0b953aatagen2026-10-10 14:41 UTC318
        .header(header::RANGE)
d46f9d0b953aatagen2026-10-10 14:41 UTC319
        .and_then(|range| parse_inch_range(range, inches))
782069860236atagen2026-10-10 12:44 UTC320
    {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC321
        match range {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC322
            InchRange::Satisfiable(start, end) => {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC323
                return partial_inches(&body, 0, start, end, inches);
ec6fd4bb5b60atagen2026-10-11 04:34 UTC324
            }
ec6fd4bb5b60atagen2026-10-11 04:34 UTC325
            InchRange::Unsatisfiable => return unsatisfied_range(inches),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC326
        }
782069860236atagen2026-10-10 12:44 UTC327
    }
782069860236atagen2026-10-10 12:44 UTC328
782069860236atagen2026-10-10 12:44 UTC329
    if run.items.ruler {
782069860236atagen2026-10-10 12:44 UTC330
        return (StatusCode::OK, [content_type, accept_ranges], body).into_response();
782069860236atagen2026-10-10 12:44 UTC331
    }
782069860236atagen2026-10-10 12:44 UTC332
    let chunks: Vec<Result<Bytes, Infallible>> = body
782069860236atagen2026-10-10 12:44 UTC333
        .into_bytes()
782069860236atagen2026-10-10 12:44 UTC334
        .chunks(16)
782069860236atagen2026-10-10 12:44 UTC335
        .map(|c| Ok(Bytes::copy_from_slice(c)))
782069860236atagen2026-10-10 12:44 UTC336
        .collect();
782069860236atagen2026-10-10 12:44 UTC337
    let body = Body::from_stream(futures_util::stream::iter(chunks));
782069860236atagen2026-10-10 12:44 UTC338
    (StatusCode::OK, [content_type, accept_ranges], body).into_response()
782069860236atagen2026-10-10 12:44 UTC339
}
782069860236atagen2026-10-10 12:44 UTC340
ec6fd4bb5b60atagen2026-10-11 04:34 UTC341
fn get_foreskin(run: &mut Run, ctx: &Ctx) -> Response {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC342
    let Some(inches) = run.foreskin else {
782069860236atagen2026-10-10 12:44 UTC343
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC344
            StatusCode::GONE,
782069860236atagen2026-10-10 12:44 UTC345
            "foreskin-gone",
782069860236atagen2026-10-10 12:44 UTC346
            "The foreskin is gone. It will not be returning.",
782069860236atagen2026-10-10 12:44 UTC347
        )
782069860236atagen2026-10-10 12:44 UTC348
        .into_response();
ec6fd4bb5b60atagen2026-10-11 04:34 UTC349
    };
ec6fd4bb5b60atagen2026-10-11 04:34 UTC350
    let body = format!("8({})D", "~".repeat(millimetres(inches)));
ec6fd4bb5b60atagen2026-10-11 04:34 UTC351
    if let Some(range) = ctx
ec6fd4bb5b60atagen2026-10-11 04:34 UTC352
        .header(header::RANGE)
ec6fd4bb5b60atagen2026-10-11 04:34 UTC353
        .and_then(|range| parse_inch_range(range, inches))
ec6fd4bb5b60atagen2026-10-11 04:34 UTC354
    {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC355
        match range {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC356
            InchRange::Satisfiable(start, end) => {
ec6fd4bb5b60atagen2026-10-11 04:34 UTC357
                return partial_inches(&body, 2, start, end, inches);
ec6fd4bb5b60atagen2026-10-11 04:34 UTC358
            }
ec6fd4bb5b60atagen2026-10-11 04:34 UTC359
            InchRange::Unsatisfiable => return unsatisfied_range(inches),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC360
        }
782069860236atagen2026-10-10 12:44 UTC361
    }
ec6fd4bb5b60atagen2026-10-11 04:34 UTC362
    (
ec6fd4bb5b60atagen2026-10-11 04:34 UTC363
        StatusCode::OK,
ec6fd4bb5b60atagen2026-10-11 04:34 UTC364
        [
ec6fd4bb5b60atagen2026-10-11 04:34 UTC365
            (header::ETAG, run.etag()),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC366
            (
ec6fd4bb5b60atagen2026-10-11 04:34 UTC367
                header::CONTENT_TYPE,
ec6fd4bb5b60atagen2026-10-11 04:34 UTC368
                "text/plain; charset=us-ascii".to_owned(),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC369
            ),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC370
            (header::ACCEPT_RANGES, "inches".to_owned()),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC371
        ],
ec6fd4bb5b60atagen2026-10-11 04:34 UTC372
        body,
ec6fd4bb5b60atagen2026-10-11 04:34 UTC373
    )
ec6fd4bb5b60atagen2026-10-11 04:34 UTC374
        .into_response()
782069860236atagen2026-10-10 12:44 UTC375
}
782069860236atagen2026-10-10 12:44 UTC376
782069860236atagen2026-10-10 12:44 UTC377
fn delete_foreskin(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC378
    respond(run.delete_foreskin(ctx.header(header::IF_MATCH)))
782069860236atagen2026-10-10 12:44 UTC379
}
782069860236atagen2026-10-10 12:44 UTC380
782069860236atagen2026-10-10 12:44 UTC381
fn get_inventory(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC382
    let items: Vec<Value> = run
782069860236atagen2026-10-10 12:44 UTC383
        .items
782069860236atagen2026-10-10 12:44 UTC384
        .owned()
782069860236atagen2026-10-10 12:44 UTC385
        .into_iter()
782069860236atagen2026-10-10 12:44 UTC386
        .map(|item| {
782069860236atagen2026-10-10 12:44 UTC387
            json!({
782069860236atagen2026-10-10 12:44 UTC388
                "name": item.slug(),
782069860236atagen2026-10-10 12:44 UTC389
                "href": run.path(&format!("/inventory/{}", item.slug())),
782069860236atagen2026-10-10 12:44 UTC390
            })
782069860236atagen2026-10-10 12:44 UTC391
        })
782069860236atagen2026-10-10 12:44 UTC392
        .collect();
782069860236atagen2026-10-10 12:44 UTC393
    Json(json!({
782069860236atagen2026-10-10 12:44 UTC394
        "items": items,
782069860236atagen2026-10-10 12:44 UTC395
        "ice_packs": run.items.ice_packs,
782069860236atagen2026-10-10 12:44 UTC396
        "supplements": run.items.supplements,
782069860236atagen2026-10-10 12:44 UTC397
        "excuses": run.excuses(),
782069860236atagen2026-10-10 12:44 UTC398
    }))
782069860236atagen2026-10-10 12:44 UTC399
    .into_response()
782069860236atagen2026-10-10 12:44 UTC400
}
782069860236atagen2026-10-10 12:44 UTC401
782069860236atagen2026-10-10 12:44 UTC402
fn owned_item(run: &Run, ctx: &Ctx) -> Result<Item, Problem> {
782069860236atagen2026-10-10 12:44 UTC403
    Item::parse(ctx.param("item"))
782069860236atagen2026-10-10 12:44 UTC404
        .filter(|&item| run.items.has(item))
782069860236atagen2026-10-10 12:44 UTC405
        .ok_or_else(|| {
782069860236atagen2026-10-10 12:44 UTC406
            Problem::new(
782069860236atagen2026-10-10 12:44 UTC407
                StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC408
                "no-such-item",
782069860236atagen2026-10-10 12:44 UTC409
                "You do not have that.",
782069860236atagen2026-10-10 12:44 UTC410
            )
782069860236atagen2026-10-10 12:44 UTC411
        })
782069860236atagen2026-10-10 12:44 UTC412
}
782069860236atagen2026-10-10 12:44 UTC413
782069860236atagen2026-10-10 12:44 UTC414
fn get_item(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC415
    match owned_item(run, ctx) {
782069860236atagen2026-10-10 12:44 UTC416
        Ok(item) => {
782069860236atagen2026-10-10 12:44 UTC417
            Json(json!({ "name": item.slug(), "description": item.description() })).into_response()
782069860236atagen2026-10-10 12:44 UTC418
        }
782069860236atagen2026-10-10 12:44 UTC419
        Err(problem) => problem.into_response(),
782069860236atagen2026-10-10 12:44 UTC420
    }
782069860236atagen2026-10-10 12:44 UTC421
}
782069860236atagen2026-10-10 12:44 UTC422
782069860236atagen2026-10-10 12:44 UTC423
fn delete_item(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC424
    match owned_item(run, ctx) {
782069860236atagen2026-10-10 12:44 UTC425
        Ok(item) => respond(run.drop_item(item)),
782069860236atagen2026-10-10 12:44 UTC426
        Err(problem) => problem.into_response(),
782069860236atagen2026-10-10 12:44 UTC427
    }
782069860236atagen2026-10-10 12:44 UTC428
}
782069860236atagen2026-10-10 12:44 UTC429
782069860236atagen2026-10-10 12:44 UTC430
fn get_techniques(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC431
    let listed: Vec<Value> = Technique::ALL
782069860236atagen2026-10-10 12:44 UTC432
        .into_iter()
b85a0d4536c3atagen2026-10-11 08:50 UTC433
        .map(|t| t.describe(run.uli_taught))
782069860236atagen2026-10-10 12:44 UTC434
        .collect();
782069860236atagen2026-10-10 12:44 UTC435
    Json(json!({
782069860236atagen2026-10-10 12:44 UTC436
        "usage": format!("JELQ {}?technique=<name>[,<name>]&intensity=<1-10>", run.path("")),
782069860236atagen2026-10-10 12:44 UTC437
        "lubricant": "Declared with Content-Type.",
782069860236atagen2026-10-10 12:44 UTC438
        "lubricants": LUBRICANTS,
782069860236atagen2026-10-10 12:44 UTC439
        "eq_band": [EQ_BAND.start(), EQ_BAND.end()],
782069860236atagen2026-10-10 12:44 UTC440
        "techniques": listed,
782069860236atagen2026-10-10 12:44 UTC441
    }))
782069860236atagen2026-10-10 12:44 UTC442
    .into_response()
782069860236atagen2026-10-10 12:44 UTC443
}
782069860236atagen2026-10-10 12:44 UTC444
782069860236atagen2026-10-10 12:44 UTC445
fn search_drawer(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC446
    respond(run.search_drawer())
782069860236atagen2026-10-10 12:44 UTC447
}
782069860236atagen2026-10-10 12:44 UTC448
782069860236atagen2026-10-10 12:44 UTC449
fn choose_branch(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC450
    respond(run.choose(ctx.param("choice")))
782069860236atagen2026-10-10 12:44 UTC451
}
782069860236atagen2026-10-10 12:44 UTC452
782069860236atagen2026-10-10 12:44 UTC453
fn get_floor(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC454
    Json(run.floor_view()).into_response()
782069860236atagen2026-10-10 12:44 UTC455
}
782069860236atagen2026-10-10 12:44 UTC456
782069860236atagen2026-10-10 12:44 UTC457
fn leave_floor(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC458
    respond(run.advance())
782069860236atagen2026-10-10 12:44 UTC459
}
782069860236atagen2026-10-10 12:44 UTC460
782069860236atagen2026-10-10 12:44 UTC461
fn get_shop(run: &mut Run, _: &Ctx) -> Response {
7d3fb63208bfatagen2026-10-11 04:38 UTC462
    let view = run.shop_view();
7d3fb63208bfatagen2026-10-11 04:38 UTC463
    if view.is_ok() {
7d3fb63208bfatagen2026-10-11 04:38 UTC464
        run.here.shopping = true;
7d3fb63208bfatagen2026-10-11 04:38 UTC465
    }
7d3fb63208bfatagen2026-10-11 04:38 UTC466
    respond(view.map(Json))
782069860236atagen2026-10-10 12:44 UTC467
}
782069860236atagen2026-10-10 12:44 UTC468
782069860236atagen2026-10-10 12:44 UTC469
fn buy(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC470
    respond(run.buy(ctx.param("item")))
782069860236atagen2026-10-10 12:44 UTC471
}
782069860236atagen2026-10-10 12:44 UTC472
0a0bfee8329eatagen2026-10-11 05:11 UTC473
fn raid_kitchen(run: &mut Run, _: &Ctx) -> Response {
0a0bfee8329eatagen2026-10-11 05:11 UTC474
    respond(run.raid_kitchen())
0a0bfee8329eatagen2026-10-11 05:11 UTC475
}
0a0bfee8329eatagen2026-10-11 05:11 UTC476
0a0bfee8329eatagen2026-10-11 05:11 UTC477
fn commit_melon(run: &mut Run, _: &Ctx) -> Response {
0a0bfee8329eatagen2026-10-11 05:11 UTC478
    respond(run.commit_melon())
0a0bfee8329eatagen2026-10-11 05:11 UTC479
}
0a0bfee8329eatagen2026-10-11 05:11 UTC480
b85a0d4536c3atagen2026-10-11 08:50 UTC481
fn re_read(run: &mut Run, _: &Ctx) -> Response {
b85a0d4536c3atagen2026-10-11 08:50 UTC482
    respond(run.read_thread())
b85a0d4536c3atagen2026-10-11 08:50 UTC483
}
b85a0d4536c3atagen2026-10-11 08:50 UTC484
782069860236atagen2026-10-10 12:44 UTC485
fn get_portal(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC486
    Json(json!({
782069860236atagen2026-10-10 12:44 UTC487
        "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.",
782069860236atagen2026-10-10 12:44 UTC488
        "accept": { "href": run.path("/portal"), "method": "POST" },
782069860236atagen2026-10-10 12:44 UTC489
    }))
782069860236atagen2026-10-10 12:44 UTC490
    .into_response()
782069860236atagen2026-10-10 12:44 UTC491
}
782069860236atagen2026-10-10 12:44 UTC492
782069860236atagen2026-10-10 12:44 UTC493
fn accept_portal(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC494
    respond(run.accept_terms())
782069860236atagen2026-10-10 12:44 UTC495
}
782069860236atagen2026-10-10 12:44 UTC496
782069860236atagen2026-10-10 12:44 UTC497
fn get_elder(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC498
    respond(run.elder(
782069860236atagen2026-10-10 12:44 UTC499
        ctx.header(header::PROXY_AUTHORIZATION),
782069860236atagen2026-10-10 12:44 UTC500
        ctx.header(header::IF_MODIFIED_SINCE),
782069860236atagen2026-10-10 12:44 UTC501
    ))
782069860236atagen2026-10-10 12:44 UTC502
}
782069860236atagen2026-10-10 12:44 UTC503
782069860236atagen2026-10-10 12:44 UTC504
fn requested_grip(ctx: &Ctx) -> Option<Grip> {
782069860236atagen2026-10-10 12:44 UTC505
    let connection = ctx.header(header::CONNECTION)?.to_ascii_lowercase();
782069860236atagen2026-10-10 12:44 UTC506
    if !connection.split(',').any(|token| token.trim() == "upgrade") {
782069860236atagen2026-10-10 12:44 UTC507
        return None;
782069860236atagen2026-10-10 12:44 UTC508
    }
782069860236atagen2026-10-10 12:44 UTC509
    ctx.header(header::UPGRADE)?
782069860236atagen2026-10-10 12:44 UTC510
        .split(',')
782069860236atagen2026-10-10 12:44 UTC511
        .find_map(|protocol| Grip::parse(protocol.trim()))
782069860236atagen2026-10-10 12:44 UTC512
}
782069860236atagen2026-10-10 12:44 UTC513
782069860236atagen2026-10-10 12:44 UTC514
fn get_grip(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC515
    if let Some(grip) = requested_grip(ctx) {
058650ec79d6atagen2026-10-12 10:49 UTC516
        if let Err(problem) = run.set_grip(grip) {
058650ec79d6atagen2026-10-12 10:49 UTC517
            return problem.into_response();
058650ec79d6atagen2026-10-12 10:49 UTC518
        }
782069860236atagen2026-10-10 12:44 UTC519
        let mut response = (
782069860236atagen2026-10-10 12:44 UTC520
            StatusCode::SWITCHING_PROTOCOLS,
782069860236atagen2026-10-10 12:44 UTC521
            [
782069860236atagen2026-10-10 12:44 UTC522
                (header::UPGRADE, grip.name()),
782069860236atagen2026-10-10 12:44 UTC523
                (header::CONNECTION, "upgrade"),
782069860236atagen2026-10-10 12:44 UTC524
            ],
782069860236atagen2026-10-10 12:44 UTC525
        )
782069860236atagen2026-10-10 12:44 UTC526
            .into_response();
782069860236atagen2026-10-10 12:44 UTC527
        response.extensions_mut().insert(Greeting(format!(
782069860236atagen2026-10-10 12:44 UTC528
            "You are now jelqing {}.\r\n",
782069860236atagen2026-10-10 12:44 UTC529
            grip.name()
782069860236atagen2026-10-10 12:44 UTC530
        )));
782069860236atagen2026-10-10 12:44 UTC531
        return response;
782069860236atagen2026-10-10 12:44 UTC532
    }
782069860236atagen2026-10-10 12:44 UTC533
    let detail = match ctx.header(header::UPGRADE) {
782069860236atagen2026-10-10 12:44 UTC534
        Some(protocol) => {
782069860236atagen2026-10-10 12:44 UTC535
            format!("The server does not know the {protocol} grip. Grips are protocols.")
782069860236atagen2026-10-10 12:44 UTC536
        }
782069860236atagen2026-10-10 12:44 UTC537
        None => "Grips are protocols. Change grip by upgrading the connection.".to_owned(),
782069860236atagen2026-10-10 12:44 UTC538
    };
782069860236atagen2026-10-10 12:44 UTC539
    (
782069860236atagen2026-10-10 12:44 UTC540
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC541
        [(header::UPGRADE, "overhand, underhand")],
782069860236atagen2026-10-10 12:44 UTC542
        Json(json!({ "grip": run.grip.name(), "detail": detail })),
782069860236atagen2026-10-10 12:44 UTC543
    )
782069860236atagen2026-10-10 12:44 UTC544
        .into_response()
782069860236atagen2026-10-10 12:44 UTC545
}
782069860236atagen2026-10-10 12:44 UTC546
782069860236atagen2026-10-10 12:44 UTC547
fn get_diagnosis(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC548
    respond(run.diagnose(
782069860236atagen2026-10-10 12:44 UTC549
        ctx.header(header::AUTHORIZATION),
782069860236atagen2026-10-10 12:44 UTC550
        ctx.header(header::IF_NONE_MATCH),
782069860236atagen2026-10-10 12:44 UTC551
    ))
782069860236atagen2026-10-10 12:44 UTC552
}
782069860236atagen2026-10-10 12:44 UTC553
782069860236atagen2026-10-10 12:44 UTC554
fn get_footage(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC555
    let Some((filmed, age)) = run.footage() else {
782069860236atagen2026-10-10 12:44 UTC556
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC557
            StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC558
            "no-footage",
782069860236atagen2026-10-10 12:44 UTC559
            "There is no footage. Yet.",
782069860236atagen2026-10-10 12:44 UTC560
        )
782069860236atagen2026-10-10 12:44 UTC561
        .into_response();
782069860236atagen2026-10-10 12:44 UTC562
    };
782069860236atagen2026-10-10 12:44 UTC563
    (
782069860236atagen2026-10-10 12:44 UTC564
        [
782069860236atagen2026-10-10 12:44 UTC565
            (
782069860236atagen2026-10-10 12:44 UTC566
                header::CACHE_CONTROL,
782069860236atagen2026-10-10 12:44 UTC567
                "public, max-age=31536000, immutable".to_owned(),
782069860236atagen2026-10-10 12:44 UTC568
            ),
782069860236atagen2026-10-10 12:44 UTC569
            (header::AGE, age.to_string()),
782069860236atagen2026-10-10 12:44 UTC570
            (header::LAST_MODIFIED, httpdate::fmt_http_date(filmed)),
782069860236atagen2026-10-10 12:44 UTC571
        ],
782069860236atagen2026-10-10 12:44 UTC572
        Json(json!({
782069860236atagen2026-10-10 12:44 UTC573
            "detail": "A recording. It is grainy. It is unmistakably you. It has been shared.",
782069860236atagen2026-10-10 12:44 UTC574
            "views": 1 + age * 37,
782069860236atagen2026-10-10 12:44 UTC575
        })),
782069860236atagen2026-10-10 12:44 UTC576
    )
782069860236atagen2026-10-10 12:44 UTC577
        .into_response()
782069860236atagen2026-10-10 12:44 UTC578
}