atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
bf5f8cb6f60f6e974991335cdfec63b7874a37bf
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 axum::{
782069860236atagen2026-10-10 12:44 UTC4
    Json,
782069860236atagen2026-10-10 12:44 UTC5
    http::{StatusCode, header},
782069860236atagen2026-10-10 12:44 UTC6
    response::{IntoResponse, Response},
782069860236atagen2026-10-10 12:44 UTC7
};
782069860236atagen2026-10-10 12:44 UTC8
use serde_json::{Value, json};
782069860236atagen2026-10-10 12:44 UTC9
782069860236atagen2026-10-10 12:44 UTC10
use super::{
782069860236atagen2026-10-10 12:44 UTC11
    ctx::Ctx,
8b8e83329a1datagen2026-10-12 12:01 UTC12
    inventory::{delete_item, get_inventory, get_item},
8b8e83329a1datagen2026-10-12 12:01 UTC13
    parse::{parse_intensity, parse_techniques},
782069860236atagen2026-10-10 12:44 UTC14
    resource::{Resource, Verb},
782069860236atagen2026-10-10 12:44 UTC15
    response::respond,
782069860236atagen2026-10-10 12:44 UTC16
    run::{Greeting, RunResource, Scope},
8b8e83329a1datagen2026-10-12 12:01 UTC17
    shaft::{get_foreskin, get_length},
782069860236atagen2026-10-10 12:44 UTC18
};
782069860236atagen2026-10-10 12:44 UTC19
use crate::{
782069860236atagen2026-10-10 12:44 UTC20
    game::{
8b8e83329a1datagen2026-10-12 12:01 UTC21
        Branch, Cause, Floor, Grip, Item, LUBRICANTS, Lube, PatchOp, Run, Technique,
8b8e83329a1datagen2026-10-12 12:01 UTC22
        balance::{EQ_BAND, VIEWS_PER_SECOND},
782069860236atagen2026-10-10 12:44 UTC23
    },
782069860236atagen2026-10-10 12:44 UTC24
    headers::ACCEPT_PATCH,
782069860236atagen2026-10-10 12:44 UTC25
    problem::Problem,
782069860236atagen2026-10-10 12:44 UTC26
};
782069860236atagen2026-10-10 12:44 UTC27
782069860236atagen2026-10-10 12:44 UTC28
const fn home(verbs: &'static [Verb<Run>]) -> RunResource {
782069860236atagen2026-10-10 12:44 UTC29
    RunResource {
782069860236atagen2026-10-10 12:44 UTC30
        scope: Scope::Home,
782069860236atagen2026-10-10 12:44 UTC31
        resource: Resource::new(verbs),
782069860236atagen2026-10-10 12:44 UTC32
    }
782069860236atagen2026-10-10 12:44 UTC33
}
782069860236atagen2026-10-10 12:44 UTC34
782069860236atagen2026-10-10 12:44 UTC35
const fn pockets(verbs: &'static [Verb<Run>]) -> RunResource {
782069860236atagen2026-10-10 12:44 UTC36
    RunResource {
782069860236atagen2026-10-10 12:44 UTC37
        scope: Scope::Pockets,
782069860236atagen2026-10-10 12:44 UTC38
        resource: Resource::new(verbs),
782069860236atagen2026-10-10 12:44 UTC39
    }
782069860236atagen2026-10-10 12:44 UTC40
}
782069860236atagen2026-10-10 12:44 UTC41
782069860236atagen2026-10-10 12:44 UTC42
fn not_in_clinic(run: &Run) -> bool {
782069860236atagen2026-10-10 12:44 UTC43
    run.floor != Floor::Clinic
782069860236atagen2026-10-10 12:44 UTC44
}
782069860236atagen2026-10-10 12:44 UTC45
782069860236atagen2026-10-10 12:44 UTC46
pub static RUN: RunResource = RunResource {
782069860236atagen2026-10-10 12:44 UTC47
    scope: Scope::Home,
8b8e83329a1datagen2026-10-12 12:01 UTC48
    resource: Resource::patchable(&[
8b8e83329a1datagen2026-10-12 12:01 UTC49
        Verb::new("GET", get_run),
8b8e83329a1datagen2026-10-12 12:01 UTC50
        Verb::new("JELQ", jelq).only_when(not_in_clinic, "JELQ is not performed in the clinic."),
8b8e83329a1datagen2026-10-12 12:01 UTC51
        Verb::new("HOLD", hold_run).only_when(
8b8e83329a1datagen2026-10-12 12:01 UTC52
            Run::can_hold,
8b8e83329a1datagen2026-10-12 12:01 UTC53
            "HOLD is only available above 70 EQ or at 2 stamina or less.",
8b8e83329a1datagen2026-10-12 12:01 UTC54
        ),
8b8e83329a1datagen2026-10-12 12:01 UTC55
        Verb::new("PUT", put_run),
8b8e83329a1datagen2026-10-12 12:01 UTC56
        Verb::new("PATCH", patch_run),
8b8e83329a1datagen2026-10-12 12:01 UTC57
        Verb::new("DELETE", delete_run),
8b8e83329a1datagen2026-10-12 12:01 UTC58
        Verb::new("TRACE", trace_run),
8b8e83329a1datagen2026-10-12 12:01 UTC59
    ]),
782069860236atagen2026-10-10 12:44 UTC60
};
782069860236atagen2026-10-10 12:44 UTC61
pub static LENGTH: RunResource = home(&[Verb::new("GET", get_length)]);
782069860236atagen2026-10-10 12:44 UTC62
pub static FORESKIN: RunResource = home(&[
782069860236atagen2026-10-10 12:44 UTC63
    Verb::new("GET", get_foreskin),
782069860236atagen2026-10-10 12:44 UTC64
    Verb::new("DELETE", delete_foreskin),
782069860236atagen2026-10-10 12:44 UTC65
]);
782069860236atagen2026-10-10 12:44 UTC66
pub static INVENTORY: RunResource = pockets(&[Verb::new("GET", get_inventory)]);
782069860236atagen2026-10-10 12:44 UTC67
pub static ITEM: RunResource =
782069860236atagen2026-10-10 12:44 UTC68
    pockets(&[Verb::new("GET", get_item), Verb::new("DELETE", delete_item)]);
782069860236atagen2026-10-10 12:44 UTC69
pub static TECHNIQUES: RunResource = home(&[Verb::new("GET", get_techniques)]);
782069860236atagen2026-10-10 12:44 UTC70
pub static DRAWER: RunResource = home(&[Verb::new("POST", search_drawer)]);
782069860236atagen2026-10-10 12:44 UTC71
pub static BRANCH: RunResource = home(&[Verb::new("POST", choose_branch)]);
0a0bfee8329eatagen2026-10-11 05:11 UTC72
pub static KITCHEN: RunResource = home(&[Verb::new("POST", raid_kitchen)]);
0a0bfee8329eatagen2026-10-11 05:11 UTC73
pub static MELON: RunResource = home(&[Verb::new("POST", commit_melon)]);
a11497af08aaatagen2026-10-13 12:04 UTC74
pub static THREAD: RunResource = pockets(&[Verb::new("GET", get_thread)]);
782069860236atagen2026-10-10 12:44 UTC75
pub static FLOOR: RunResource =
0d604ee39751atagen2026-10-10 14:15 UTC76
    home(&[Verb::new("GET", get_floor), Verb::new("POST", leave_floor)]);
782069860236atagen2026-10-10 12:44 UTC77
pub static SHOP: RunResource = home(&[Verb::new("GET", get_shop)]);
782069860236atagen2026-10-10 12:44 UTC78
pub static BUY: RunResource = home(&[Verb::new("POST", buy)]);
782069860236atagen2026-10-10 12:44 UTC79
pub static PORTAL: RunResource = pockets(&[
782069860236atagen2026-10-10 12:44 UTC80
    Verb::new("GET", get_portal),
782069860236atagen2026-10-10 12:44 UTC81
    Verb::new("POST", accept_portal),
782069860236atagen2026-10-10 12:44 UTC82
]);
782069860236atagen2026-10-10 12:44 UTC83
pub static ELDER: RunResource = home(&[Verb::new("GET", get_elder)]);
782069860236atagen2026-10-10 12:44 UTC84
pub static GRIP: RunResource = home(&[Verb::new("GET", get_grip)]);
782069860236atagen2026-10-10 12:44 UTC85
pub static DIAGNOSIS: RunResource = pockets(&[Verb::new("GET", get_diagnosis)]);
782069860236atagen2026-10-10 12:44 UTC86
pub static FOOTAGE: RunResource = pockets(&[
782069860236atagen2026-10-10 12:44 UTC87
    Verb::new("GET", get_footage),
8b8e83329a1datagen2026-10-12 12:01 UTC88
    Verb::refused(
8b8e83329a1datagen2026-10-12 12:01 UTC89
        "DELETE",
782069860236atagen2026-10-10 12:44 UTC90
        "The footage has been cached by intermediaries. It cannot be deleted.",
782069860236atagen2026-10-10 12:44 UTC91
    ),
782069860236atagen2026-10-10 12:44 UTC92
]);
782069860236atagen2026-10-10 12:44 UTC93
782069860236atagen2026-10-10 12:44 UTC94
fn get_run(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC95
    if ctx
782069860236atagen2026-10-10 12:44 UTC96
        .header(header::ACCEPT)
782069860236atagen2026-10-10 12:44 UTC97
        .is_some_and(|accept| accept.contains("reassurance"))
782069860236atagen2026-10-10 12:44 UTC98
    {
782069860236atagen2026-10-10 12:44 UTC99
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC100
            StatusCode::NOT_ACCEPTABLE,
782069860236atagen2026-10-10 12:44 UTC101
            "no-reassurance",
782069860236atagen2026-10-10 12:44 UTC102
            "The server does not produce reassurance.",
782069860236atagen2026-10-10 12:44 UTC103
        )
782069860236atagen2026-10-10 12:44 UTC104
        .into_response();
782069860236atagen2026-10-10 12:44 UTC105
    }
782069860236atagen2026-10-10 12:44 UTC106
    (
782069860236atagen2026-10-10 12:44 UTC107
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC108
        [(header::ETAG, run.etag())],
782069860236atagen2026-10-10 12:44 UTC109
        Json(run.view()),
782069860236atagen2026-10-10 12:44 UTC110
    )
782069860236atagen2026-10-10 12:44 UTC111
        .into_response()
782069860236atagen2026-10-10 12:44 UTC112
}
782069860236atagen2026-10-10 12:44 UTC113
782069860236atagen2026-10-10 12:44 UTC114
fn jelq(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC115
    let parsed = parse_techniques(run, ctx).and_then(|techniques| {
782069860236atagen2026-10-10 12:44 UTC116
        let intensity = parse_intensity(ctx)?;
782069860236atagen2026-10-10 12:44 UTC117
        let lube = Lube::from_content_type(ctx.header(header::CONTENT_TYPE))?;
782069860236atagen2026-10-10 12:44 UTC118
        Ok((techniques, intensity, lube))
782069860236atagen2026-10-10 12:44 UTC119
    });
782069860236atagen2026-10-10 12:44 UTC120
    match parsed {
a6caa2f99786atagen2026-10-12 10:38 UTC121
        Ok((techniques, intensity, lube)) => respond(run.jelq(&techniques, intensity, lube)),
782069860236atagen2026-10-10 12:44 UTC122
        Err(problem) => problem.into_response(),
782069860236atagen2026-10-10 12:44 UTC123
    }
782069860236atagen2026-10-10 12:44 UTC124
}
782069860236atagen2026-10-10 12:44 UTC125
782069860236atagen2026-10-10 12:44 UTC126
fn put_run(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC127
    run.put().into_response()
782069860236atagen2026-10-10 12:44 UTC128
}
782069860236atagen2026-10-10 12:44 UTC129
a6caa2f99786atagen2026-10-12 10:38 UTC130
fn hold_run(run: &mut Run, _: &Ctx) -> Response {
a6caa2f99786atagen2026-10-12 10:38 UTC131
    respond(run.hold())
4347977b8192atagen2026-10-11 04:56 UTC132
}
4347977b8192atagen2026-10-11 04:56 UTC133
782069860236atagen2026-10-10 12:44 UTC134
fn patch_run(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC135
    let is_patch = ctx
782069860236atagen2026-10-10 12:44 UTC136
        .header(header::CONTENT_TYPE)
782069860236atagen2026-10-10 12:44 UTC137
        .is_some_and(|ct| ct.starts_with("application/json-patch+json"));
782069860236atagen2026-10-10 12:44 UTC138
    if !is_patch {
782069860236atagen2026-10-10 12:44 UTC139
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC140
            StatusCode::UNSUPPORTED_MEDIA_TYPE,
782069860236atagen2026-10-10 12:44 UTC141
            "patch-format",
782069860236atagen2026-10-10 12:44 UTC142
            "Patches are accepted as application/json-patch+json.",
782069860236atagen2026-10-10 12:44 UTC143
        )
782069860236atagen2026-10-10 12:44 UTC144
        .header(ACCEPT_PATCH, "application/json-patch+json")
782069860236atagen2026-10-10 12:44 UTC145
        .into_response();
782069860236atagen2026-10-10 12:44 UTC146
    }
8b8e83329a1datagen2026-10-12 12:01 UTC147
    match serde_json::from_slice::<Vec<PatchOp>>(ctx.body()) {
782069860236atagen2026-10-10 12:44 UTC148
        Ok(ops) => respond(run.patch(&ops)),
782069860236atagen2026-10-10 12:44 UTC149
        Err(_) => Problem::new(
782069860236atagen2026-10-10 12:44 UTC150
            StatusCode::BAD_REQUEST,
782069860236atagen2026-10-10 12:44 UTC151
            "malformed-patch",
782069860236atagen2026-10-10 12:44 UTC152
            "The patch could not be parsed.",
782069860236atagen2026-10-10 12:44 UTC153
        )
782069860236atagen2026-10-10 12:44 UTC154
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC155
    }
782069860236atagen2026-10-10 12:44 UTC156
}
782069860236atagen2026-10-10 12:44 UTC157
782069860236atagen2026-10-10 12:44 UTC158
fn delete_run(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC159
    run.end(Cause::Abandoned);
782069860236atagen2026-10-10 12:44 UTC160
    StatusCode::NO_CONTENT.into_response()
782069860236atagen2026-10-10 12:44 UTC161
}
782069860236atagen2026-10-10 12:44 UTC162
782069860236atagen2026-10-10 12:44 UTC163
fn trace_run(run: &mut Run, ctx: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC164
    run.trace();
782069860236atagen2026-10-10 12:44 UTC165
    (
782069860236atagen2026-10-10 12:44 UTC166
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC167
        [(header::CONTENT_TYPE, "message/http")],
8b8e83329a1datagen2026-10-12 12:01 UTC168
        ctx.echo(),
ec6fd4bb5b60atagen2026-10-11 04:34 UTC169
    )
ec6fd4bb5b60atagen2026-10-11 04:34 UTC170
        .into_response()
782069860236atagen2026-10-10 12:44 UTC171
}
782069860236atagen2026-10-10 12:44 UTC172
782069860236atagen2026-10-10 12:44 UTC173
fn delete_foreskin(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC174
    respond(run.delete_foreskin(ctx.header(header::IF_MATCH)))
782069860236atagen2026-10-10 12:44 UTC175
}
782069860236atagen2026-10-10 12:44 UTC176
782069860236atagen2026-10-10 12:44 UTC177
fn get_techniques(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC178
    let listed: Vec<Value> = Technique::ALL
782069860236atagen2026-10-10 12:44 UTC179
        .into_iter()
880b29a0c11eatagen2026-10-13 09:27 UTC180
        .map(|t| t.describe(run.knows(t)))
782069860236atagen2026-10-10 12:44 UTC181
        .collect();
782069860236atagen2026-10-10 12:44 UTC182
    Json(json!({
782069860236atagen2026-10-10 12:44 UTC183
        "usage": format!("JELQ {}?technique=<name>[,<name>]&intensity=<1-10>", run.path("")),
782069860236atagen2026-10-10 12:44 UTC184
        "lubricant": "Declared with Content-Type.",
782069860236atagen2026-10-10 12:44 UTC185
        "lubricants": LUBRICANTS,
782069860236atagen2026-10-10 12:44 UTC186
        "eq_band": [EQ_BAND.start(), EQ_BAND.end()],
782069860236atagen2026-10-10 12:44 UTC187
        "techniques": listed,
782069860236atagen2026-10-10 12:44 UTC188
    }))
782069860236atagen2026-10-10 12:44 UTC189
    .into_response()
782069860236atagen2026-10-10 12:44 UTC190
}
782069860236atagen2026-10-10 12:44 UTC191
782069860236atagen2026-10-10 12:44 UTC192
fn search_drawer(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC193
    respond(run.search_drawer())
782069860236atagen2026-10-10 12:44 UTC194
}
782069860236atagen2026-10-10 12:44 UTC195
782069860236atagen2026-10-10 12:44 UTC196
fn choose_branch(run: &mut Run, ctx: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC197
    match ctx.param("choice").parse::<Branch>() {
8b8e83329a1datagen2026-10-12 12:01 UTC198
        Ok(choice) => respond(run.choose(choice)),
8b8e83329a1datagen2026-10-12 12:01 UTC199
        Err(()) => Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC200
            StatusCode::NOT_FOUND,
8b8e83329a1datagen2026-10-12 12:01 UTC201
            "no-such-choice",
8b8e83329a1datagen2026-10-12 12:01 UTC202
            "That was not one of the choices.",
8b8e83329a1datagen2026-10-12 12:01 UTC203
        )
8b8e83329a1datagen2026-10-12 12:01 UTC204
        .into_response(),
8b8e83329a1datagen2026-10-12 12:01 UTC205
    }
782069860236atagen2026-10-10 12:44 UTC206
}
782069860236atagen2026-10-10 12:44 UTC207
782069860236atagen2026-10-10 12:44 UTC208
fn get_floor(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC209
    Json(run.floor_view()).into_response()
782069860236atagen2026-10-10 12:44 UTC210
}
782069860236atagen2026-10-10 12:44 UTC211
782069860236atagen2026-10-10 12:44 UTC212
fn leave_floor(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC213
    respond(run.advance())
782069860236atagen2026-10-10 12:44 UTC214
}
782069860236atagen2026-10-10 12:44 UTC215
782069860236atagen2026-10-10 12:44 UTC216
fn get_shop(run: &mut Run, _: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC217
    respond(run.browse_shop().map(Json))
782069860236atagen2026-10-10 12:44 UTC218
}
782069860236atagen2026-10-10 12:44 UTC219
782069860236atagen2026-10-10 12:44 UTC220
fn buy(run: &mut Run, ctx: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC221
    respond(run.buy(Item::parse(ctx.param("item"))))
782069860236atagen2026-10-10 12:44 UTC222
}
782069860236atagen2026-10-10 12:44 UTC223
0a0bfee8329eatagen2026-10-11 05:11 UTC224
fn raid_kitchen(run: &mut Run, _: &Ctx) -> Response {
0a0bfee8329eatagen2026-10-11 05:11 UTC225
    respond(run.raid_kitchen())
0a0bfee8329eatagen2026-10-11 05:11 UTC226
}
0a0bfee8329eatagen2026-10-11 05:11 UTC227
0a0bfee8329eatagen2026-10-11 05:11 UTC228
fn commit_melon(run: &mut Run, _: &Ctx) -> Response {
0a0bfee8329eatagen2026-10-11 05:11 UTC229
    respond(run.commit_melon())
0a0bfee8329eatagen2026-10-11 05:11 UTC230
}
0a0bfee8329eatagen2026-10-11 05:11 UTC231
a11497af08aaatagen2026-10-13 12:04 UTC232
fn get_thread(run: &mut Run, _: &Ctx) -> Response {
b85a0d4536c3atagen2026-10-11 08:50 UTC233
    respond(run.read_thread())
b85a0d4536c3atagen2026-10-11 08:50 UTC234
}
b85a0d4536c3atagen2026-10-11 08:50 UTC235
782069860236atagen2026-10-10 12:44 UTC236
fn get_portal(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC237
    Json(json!({
782069860236atagen2026-10-10 12:44 UTC238
        "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 UTC239
        "accept": { "href": run.path("/portal"), "method": "POST" },
782069860236atagen2026-10-10 12:44 UTC240
    }))
782069860236atagen2026-10-10 12:44 UTC241
    .into_response()
782069860236atagen2026-10-10 12:44 UTC242
}
782069860236atagen2026-10-10 12:44 UTC243
782069860236atagen2026-10-10 12:44 UTC244
fn accept_portal(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC245
    respond(run.accept_terms())
782069860236atagen2026-10-10 12:44 UTC246
}
782069860236atagen2026-10-10 12:44 UTC247
782069860236atagen2026-10-10 12:44 UTC248
fn get_elder(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC249
    respond(run.elder(
782069860236atagen2026-10-10 12:44 UTC250
        ctx.header(header::PROXY_AUTHORIZATION),
782069860236atagen2026-10-10 12:44 UTC251
        ctx.header(header::IF_MODIFIED_SINCE),
782069860236atagen2026-10-10 12:44 UTC252
    ))
782069860236atagen2026-10-10 12:44 UTC253
}
782069860236atagen2026-10-10 12:44 UTC254
782069860236atagen2026-10-10 12:44 UTC255
fn requested_grip(ctx: &Ctx) -> Option<Grip> {
782069860236atagen2026-10-10 12:44 UTC256
    let connection = ctx.header(header::CONNECTION)?.to_ascii_lowercase();
782069860236atagen2026-10-10 12:44 UTC257
    if !connection.split(',').any(|token| token.trim() == "upgrade") {
782069860236atagen2026-10-10 12:44 UTC258
        return None;
782069860236atagen2026-10-10 12:44 UTC259
    }
782069860236atagen2026-10-10 12:44 UTC260
    ctx.header(header::UPGRADE)?
782069860236atagen2026-10-10 12:44 UTC261
        .split(',')
8b8e83329a1datagen2026-10-12 12:01 UTC262
        .find_map(|protocol| protocol.trim().parse::<Grip>().ok())
782069860236atagen2026-10-10 12:44 UTC263
}
782069860236atagen2026-10-10 12:44 UTC264
782069860236atagen2026-10-10 12:44 UTC265
fn get_grip(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC266
    if let Some(grip) = requested_grip(ctx) {
058650ec79d6atagen2026-10-12 10:49 UTC267
        if let Err(problem) = run.set_grip(grip) {
058650ec79d6atagen2026-10-12 10:49 UTC268
            return problem.into_response();
058650ec79d6atagen2026-10-12 10:49 UTC269
        }
782069860236atagen2026-10-10 12:44 UTC270
        let mut response = (
782069860236atagen2026-10-10 12:44 UTC271
            StatusCode::SWITCHING_PROTOCOLS,
782069860236atagen2026-10-10 12:44 UTC272
            [
782069860236atagen2026-10-10 12:44 UTC273
                (header::UPGRADE, grip.name()),
782069860236atagen2026-10-10 12:44 UTC274
                (header::CONNECTION, "upgrade"),
782069860236atagen2026-10-10 12:44 UTC275
            ],
782069860236atagen2026-10-10 12:44 UTC276
        )
782069860236atagen2026-10-10 12:44 UTC277
            .into_response();
782069860236atagen2026-10-10 12:44 UTC278
        response.extensions_mut().insert(Greeting(format!(
782069860236atagen2026-10-10 12:44 UTC279
            "You are now jelqing {}.\r\n",
782069860236atagen2026-10-10 12:44 UTC280
            grip.name()
782069860236atagen2026-10-10 12:44 UTC281
        )));
782069860236atagen2026-10-10 12:44 UTC282
        return response;
782069860236atagen2026-10-10 12:44 UTC283
    }
782069860236atagen2026-10-10 12:44 UTC284
    let detail = match ctx.header(header::UPGRADE) {
782069860236atagen2026-10-10 12:44 UTC285
        Some(protocol) => {
782069860236atagen2026-10-10 12:44 UTC286
            format!("The server does not know the {protocol} grip. Grips are protocols.")
782069860236atagen2026-10-10 12:44 UTC287
        }
782069860236atagen2026-10-10 12:44 UTC288
        None => "Grips are protocols. Change grip by upgrading the connection.".to_owned(),
782069860236atagen2026-10-10 12:44 UTC289
    };
782069860236atagen2026-10-10 12:44 UTC290
    (
782069860236atagen2026-10-10 12:44 UTC291
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC292
        [(header::UPGRADE, "overhand, underhand")],
8b8e83329a1datagen2026-10-12 12:01 UTC293
        Json(json!({ "grip": run.grip().name(), "detail": detail })),
782069860236atagen2026-10-10 12:44 UTC294
    )
782069860236atagen2026-10-10 12:44 UTC295
        .into_response()
782069860236atagen2026-10-10 12:44 UTC296
}
782069860236atagen2026-10-10 12:44 UTC297
782069860236atagen2026-10-10 12:44 UTC298
fn get_diagnosis(run: &mut Run, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC299
    respond(run.diagnose(
782069860236atagen2026-10-10 12:44 UTC300
        ctx.header(header::AUTHORIZATION),
782069860236atagen2026-10-10 12:44 UTC301
        ctx.header(header::IF_NONE_MATCH),
782069860236atagen2026-10-10 12:44 UTC302
    ))
782069860236atagen2026-10-10 12:44 UTC303
}
782069860236atagen2026-10-10 12:44 UTC304
782069860236atagen2026-10-10 12:44 UTC305
fn get_footage(run: &mut Run, _: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC306
    let Some((filmed, age)) = run.footage() else {
782069860236atagen2026-10-10 12:44 UTC307
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC308
            StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC309
            "no-footage",
782069860236atagen2026-10-10 12:44 UTC310
            "There is no footage. Yet.",
782069860236atagen2026-10-10 12:44 UTC311
        )
782069860236atagen2026-10-10 12:44 UTC312
        .into_response();
782069860236atagen2026-10-10 12:44 UTC313
    };
782069860236atagen2026-10-10 12:44 UTC314
    (
782069860236atagen2026-10-10 12:44 UTC315
        [
782069860236atagen2026-10-10 12:44 UTC316
            (
782069860236atagen2026-10-10 12:44 UTC317
                header::CACHE_CONTROL,
782069860236atagen2026-10-10 12:44 UTC318
                "public, max-age=31536000, immutable".to_owned(),
782069860236atagen2026-10-10 12:44 UTC319
            ),
782069860236atagen2026-10-10 12:44 UTC320
            (header::AGE, age.to_string()),
782069860236atagen2026-10-10 12:44 UTC321
            (header::LAST_MODIFIED, httpdate::fmt_http_date(filmed)),
782069860236atagen2026-10-10 12:44 UTC322
        ],
782069860236atagen2026-10-10 12:44 UTC323
        Json(json!({
782069860236atagen2026-10-10 12:44 UTC324
            "detail": "A recording. It is grainy. It is unmistakably you. It has been shared.",
8b8e83329a1datagen2026-10-12 12:01 UTC325
            "views": 1 + age * VIEWS_PER_SECOND,
782069860236atagen2026-10-10 12:44 UTC326
        })),
782069860236atagen2026-10-10 12:44 UTC327
    )
782069860236atagen2026-10-10 12:44 UTC328
        .into_response()
782069860236atagen2026-10-10 12:44 UTC329
}