atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/http/run.rs

782069860236atagen2026-10-10 12:44 UTC1
//! Plumbing shared by every resource under `/runs/{id}`.
782069860236atagen2026-10-10 12:44 UTC2
58e1fb3943e6atagen2026-10-10 13:30 UTC3
use std::time::Instant;
58e1fb3943e6atagen2026-10-10 13:30 UTC4
782069860236atagen2026-10-10 12:44 UTC5
use axum::{
782069860236atagen2026-10-10 12:44 UTC6
    http::{HeaderName, HeaderValue, StatusCode, header},
782069860236atagen2026-10-10 12:44 UTC7
    response::{IntoResponse, Response},
782069860236atagen2026-10-10 12:44 UTC8
};
782069860236atagen2026-10-10 12:44 UTC9
use hyper_util::rt::TokioIo;
782069860236atagen2026-10-10 12:44 UTC10
use tokio::io::AsyncWriteExt;
782069860236atagen2026-10-10 12:44 UTC11
d604371299fbatagen2026-10-10 13:54 UTC12
use super::{AppState, ctx::Ctx, meta, middleware::Compress, resource::Resource, runs};
782069860236atagen2026-10-10 12:44 UTC13
use crate::{
0fdc47364c8catagen2026-10-11 04:45 UTC14
    game::{
0fdc47364c8catagen2026-10-11 04:45 UTC15
        Elder, FORUM_THREAD, Floor, Run, SHOP, Story,
e807266c323batagen2026-10-12 09:46 UTC16
        balance::{FRICTION_MAX, KITCHEN_TELLINGS, SUSPICION_LIMIT},
0fdc47364c8catagen2026-10-11 04:45 UTC17
    },
782069860236atagen2026-10-10 12:44 UTC18
    headers::{
5dfebcc0ac43atagen2026-10-11 05:28 UTC19
        X_CASH, X_EQ, X_FLOOR, X_FORESKIN, X_FRICTION, X_GIRTH, X_LENGTH, X_OBSERVED_BY,
f39b10e20a0batagen2026-10-10 14:33 UTC20
        X_SELF_REPORTED_LENGTH, X_SHAME, X_STAMINA, X_SUSPICION,
782069860236atagen2026-10-10 12:44 UTC21
    },
ef7d6dee5976atagen2026-10-10 13:23 UTC22
    locale::Locale,
782069860236atagen2026-10-10 12:44 UTC23
    problem::{PROBLEM_BASE, Problem},
782069860236atagen2026-10-10 12:44 UTC24
    secret::Secret,
782069860236atagen2026-10-10 12:44 UTC25
};
782069860236atagen2026-10-10 12:44 UTC26
782069860236atagen2026-10-10 12:44 UTC27
pub const REL_BASE: &str = "https://jelq.dev/rels/";
782069860236atagen2026-10-10 12:44 UTC28
const COOKIE_ATTRIBUTES: &str = "Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict";
782069860236atagen2026-10-10 12:44 UTC29
/// Which household hazards stand between a request and the run.
782069860236atagen2026-10-10 12:44 UTC30
#[derive(Clone, Copy, PartialEq, Eq)]
782069860236atagen2026-10-10 12:44 UTC31
pub enum Scope {
782069860236atagen2026-10-10 12:44 UTC32
    Home,
782069860236atagen2026-10-10 12:44 UTC33
    Pockets,
782069860236atagen2026-10-10 12:44 UTC34
}
782069860236atagen2026-10-10 12:44 UTC35
782069860236atagen2026-10-10 12:44 UTC36
pub struct RunResource {
782069860236atagen2026-10-10 12:44 UTC37
    pub scope: Scope,
782069860236atagen2026-10-10 12:44 UTC38
    pub resource: Resource<Run>,
782069860236atagen2026-10-10 12:44 UTC39
}
782069860236atagen2026-10-10 12:44 UTC40
782069860236atagen2026-10-10 12:44 UTC41
/// What the server writes over the connection after a `101`.
782069860236atagen2026-10-10 12:44 UTC42
#[derive(Clone)]
782069860236atagen2026-10-10 12:44 UTC43
pub struct Greeting(pub String);
782069860236atagen2026-10-10 12:44 UTC44
782069860236atagen2026-10-10 12:44 UTC45
pub fn serve(state: &AppState, mut ctx: Ctx, target: &RunResource) -> Response {
782069860236atagen2026-10-10 12:44 UTC46
    let mut runs = state.runs();
782069860236atagen2026-10-10 12:44 UTC47
    let Some(run) = runs.get_mut(ctx.param("id")) else {
782069860236atagen2026-10-10 12:44 UTC48
        return Problem::new(
782069860236atagen2026-10-10 12:44 UTC49
            StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC50
            "no-such-run",
782069860236atagen2026-10-10 12:44 UTC51
            "There is no such run. There may never have been.",
782069860236atagen2026-10-10 12:44 UTC52
        )
782069860236atagen2026-10-10 12:44 UTC53
        .into_response();
782069860236atagen2026-10-10 12:44 UTC54
    };
782069860236atagen2026-10-10 12:44 UTC55
    if let Some(ending) = &run.ended {
782069860236atagen2026-10-10 12:44 UTC56
        return Problem::new(StatusCode::GONE, "run-ended", "This run has ended.")
782069860236atagen2026-10-10 12:44 UTC57
            .with("cause", format!("{PROBLEM_BASE}{}", ending.cause.slug()))
782069860236atagen2026-10-10 12:44 UTC58
            .with("turn", ending.turn)
782069860236atagen2026-10-10 12:44 UTC59
            .with("graveyard", "/graveyard")
782069860236atagen2026-10-10 12:44 UTC60
            .into_response();
782069860236atagen2026-10-10 12:44 UTC61
    }
782069860236atagen2026-10-10 12:44 UTC62
    run.last_seen = ctx.now;
e76ab808e7adatagen2026-10-10 13:46 UTC63
    run.recover(ctx.now);
782069860236atagen2026-10-10 12:44 UTC64
    run.observed = None;
782069860236atagen2026-10-10 12:44 UTC65
    let response =
782069860236atagen2026-10-10 12:44 UTC66
        gate(run, &ctx, target.scope).unwrap_or_else(|| target.resource.dispatch(run, &ctx));
782069860236atagen2026-10-10 12:44 UTC67
    let response = finish(run, &ctx, &state.config.secret, response);
782069860236atagen2026-10-10 12:44 UTC68
    drop(runs);
782069860236atagen2026-10-10 12:44 UTC69
    greet(response, &mut ctx)
782069860236atagen2026-10-10 12:44 UTC70
}
782069860236atagen2026-10-10 12:44 UTC71
782069860236atagen2026-10-10 12:44 UTC72
fn gate(run: &mut Run, ctx: &Ctx, scope: Scope) -> Option<Response> {
782069860236atagen2026-10-10 12:44 UTC73
    if scope == Scope::Pockets {
782069860236atagen2026-10-10 12:44 UTC74
        return None;
782069860236atagen2026-10-10 12:44 UTC75
    }
782069860236atagen2026-10-10 12:44 UTC76
    if run.floor == Floor::Hotel && !run.here.wifi_accepted {
782069860236atagen2026-10-10 12:44 UTC77
        let portal = run.path("/portal");
782069860236atagen2026-10-10 12:44 UTC78
        return Some(
782069860236atagen2026-10-10 12:44 UTC79
            Problem::new(
782069860236atagen2026-10-10 12:44 UTC80
                StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
782069860236atagen2026-10-10 12:44 UTC81
                "captive-portal",
782069860236atagen2026-10-10 12:44 UTC82
                "The network requires authentication. The hotel would like you to agree to some terms.",
782069860236atagen2026-10-10 12:44 UTC83
            )
782069860236atagen2026-10-10 12:44 UTC84
            .with("portal", portal.as_str())
782069860236atagen2026-10-10 12:44 UTC85
            .header(header::LINK, format!("<{portal}>; rel=\"{REL_BASE}portal\""))
782069860236atagen2026-10-10 12:44 UTC86
            .into_response(),
782069860236atagen2026-10-10 12:44 UTC87
        );
782069860236atagen2026-10-10 12:44 UTC88
    }
ef7d6dee5976atagen2026-10-10 13:23 UTC89
    let authorization = ctx
ef7d6dee5976atagen2026-10-10 13:23 UTC90
        .header(header::AUTHORIZATION)
ef7d6dee5976atagen2026-10-10 13:23 UTC91
        .map(|text| Locale::negotiate(ctx.header(header::ACCEPT_LANGUAGE)).canonicalise(text));
ef7d6dee5976atagen2026-10-10 13:23 UTC92
    run.answer_door(authorization.as_deref())
782069860236atagen2026-10-10 12:44 UTC93
        .err()
782069860236atagen2026-10-10 12:44 UTC94
        .map(IntoResponse::into_response)
782069860236atagen2026-10-10 12:44 UTC95
}
782069860236atagen2026-10-10 12:44 UTC96
58e1fb3943e6atagen2026-10-10 13:30 UTC97
/// Every resource the run can reach right now, then every technique a JELQ could use. The `allow`
58e1fb3943e6atagen2026-10-10 13:30 UTC98
/// and `lubricant` extension attributes (RFC 8288 §3) say what each accepts, so a client never guesses.
58e1fb3943e6atagen2026-10-10 13:30 UTC99
fn links(run: &Run, now: Instant) -> String {
e807266c323batagen2026-10-12 09:46 UTC100
    let thread_reachable = run.thread_reachable();
b85a0d4536c3atagen2026-10-11 08:50 UTC101
    let linked: [(&str, &RunResource, bool); 15] = [
185851447cebatagen2026-10-10 12:58 UTC102
        ("", &runs::RUN, true),
185851447cebatagen2026-10-10 12:58 UTC103
        ("floor", &runs::FLOOR, true),
185851447cebatagen2026-10-10 12:58 UTC104
        ("length", &runs::LENGTH, true),
185851447cebatagen2026-10-10 12:58 UTC105
        ("foreskin", &runs::FORESKIN, run.foreskin.is_some()),
185851447cebatagen2026-10-10 12:58 UTC106
        ("inventory", &runs::INVENTORY, true),
185851447cebatagen2026-10-10 12:58 UTC107
        ("techniques", &runs::TECHNIQUES, true),
d604371299fbatagen2026-10-10 13:54 UTC108
        ("drawer", &runs::DRAWER, run.drawer_worth_searching()),
0a0bfee8329eatagen2026-10-11 05:11 UTC109
        ("kitchen", &runs::KITCHEN, {
0a0bfee8329eatagen2026-10-11 05:11 UTC110
            run.floor.has_kitchen() && run.kitchen_tellings < KITCHEN_TELLINGS
0a0bfee8329eatagen2026-10-11 05:11 UTC111
        }),
b85a0d4536c3atagen2026-10-11 08:50 UTC112
        ("re-read", &runs::REREAD, thread_reachable),
185851447cebatagen2026-10-10 12:58 UTC113
        ("grip", &runs::GRIP, true),
185851447cebatagen2026-10-10 12:58 UTC114
        ("shop", &runs::SHOP, run.floor.shop_open()),
d604371299fbatagen2026-10-10 13:54 UTC115
        (
d604371299fbatagen2026-10-10 13:54 UTC116
            "portal",
d604371299fbatagen2026-10-10 13:54 UTC117
            &runs::PORTAL,
d604371299fbatagen2026-10-10 13:54 UTC118
            run.floor == Floor::Hotel && !run.here.wifi_accepted,
d604371299fbatagen2026-10-10 13:54 UTC119
        ),
d604371299fbatagen2026-10-10 13:54 UTC120
        (
d604371299fbatagen2026-10-10 13:54 UTC121
            "elder",
d604371299fbatagen2026-10-10 13:54 UTC122
            &runs::ELDER,
d604371299fbatagen2026-10-10 13:54 UTC123
            run.floor == Floor::Forum && run.elder != Elder::Beaten,
d604371299fbatagen2026-10-10 13:54 UTC124
        ),
185851447cebatagen2026-10-10 12:58 UTC125
        ("footage", &runs::FOOTAGE, run.filmed_at.is_some()),
d604371299fbatagen2026-10-10 13:54 UTC126
        ("diagnosis", &runs::DIAGNOSIS, run.floor == Floor::Clinic),
185851447cebatagen2026-10-10 12:58 UTC127
    ];
185851447cebatagen2026-10-10 12:58 UTC128
    linked
185851447cebatagen2026-10-10 12:58 UTC129
        .into_iter()
185851447cebatagen2026-10-10 12:58 UTC130
        .filter(|(_, _, relevant)| *relevant)
185851447cebatagen2026-10-10 12:58 UTC131
        .map(|(rel, target, _)| {
185851447cebatagen2026-10-10 12:58 UTC132
            let (path, name) = if rel.is_empty() {
185851447cebatagen2026-10-10 12:58 UTC133
                (run.path(""), "run")
185851447cebatagen2026-10-10 12:58 UTC134
            } else {
185851447cebatagen2026-10-10 12:58 UTC135
                (run.path(&format!("/{rel}")), rel)
185851447cebatagen2026-10-10 12:58 UTC136
            };
185851447cebatagen2026-10-10 12:58 UTC137
            let allow = target.resource.allow(run);
185851447cebatagen2026-10-10 12:58 UTC138
            format!("<{path}>; rel=\"{REL_BASE}{name}\"; allow=\"{allow}\"")
185851447cebatagen2026-10-10 12:58 UTC139
        })
b85a0d4536c3atagen2026-10-11 08:50 UTC140
        .chain(thread_reachable.then(|| {
0fdc47364c8catagen2026-10-11 04:45 UTC141
            format!(
0fdc47364c8catagen2026-10-11 04:45 UTC142
                "<{FORUM_THREAD}>; rel=\"{REL_BASE}thread\"; allow=\"{}\"",
0fdc47364c8catagen2026-10-11 04:45 UTC143
                meta::THREAD.allow_any()
0fdc47364c8catagen2026-10-11 04:45 UTC144
            )
0fdc47364c8catagen2026-10-11 04:45 UTC145
        }),)
d46f9d0b953aatagen2026-10-10 14:41 UTC146
        .chain((run.story == Story::Pending).then(|| {
d46f9d0b953aatagen2026-10-10 14:41 UTC147
            let choice = |slug: &str, title: &str| {
d46f9d0b953aatagen2026-10-10 14:41 UTC148
                format!(
d46f9d0b953aatagen2026-10-10 14:41 UTC149
                    "<{}>; rel=\"{REL_BASE}branch\"; title=\"{title}\"; allow=\"POST\"",
d46f9d0b953aatagen2026-10-10 14:41 UTC150
                    run.path(&format!("/branches/{slug}"))
d46f9d0b953aatagen2026-10-10 14:41 UTC151
                )
d46f9d0b953aatagen2026-10-10 14:41 UTC152
            };
d46f9d0b953aatagen2026-10-10 14:41 UTC153
            [choice("forum", "Read the forum"), choice("close-tab", "Close the tab")].join(", ")
d46f9d0b953aatagen2026-10-10 14:41 UTC154
        }))
58e1fb3943e6atagen2026-10-10 13:30 UTC155
        .chain(run.techniques_now(now).into_iter().map(|technique| {
58e1fb3943e6atagen2026-10-10 13:30 UTC156
            let name = technique.name();
58e1fb3943e6atagen2026-10-10 13:30 UTC157
            let lubricant = technique
58e1fb3943e6atagen2026-10-10 13:30 UTC158
                .lubricant()
58e1fb3943e6atagen2026-10-10 13:30 UTC159
                .map_or(String::new(), |rule| format!("; lubricant=\"{rule}\""));
58e1fb3943e6atagen2026-10-10 13:30 UTC160
            format!(
58e1fb3943e6atagen2026-10-10 13:30 UTC161
                "<{}?technique={name}>; rel=\"{REL_BASE}technique\"; title=\"{name}\"; allow=\"JELQ\"{lubricant}",
58e1fb3943e6atagen2026-10-10 13:30 UTC162
                run.path("")
58e1fb3943e6atagen2026-10-10 13:30 UTC163
            )
58e1fb3943e6atagen2026-10-10 13:30 UTC164
        }))
7d3fb63208bfatagen2026-10-11 04:38 UTC165
        .chain(run.here.shopping.then(|| {
7d3fb63208bfatagen2026-10-11 04:38 UTC166
            SHOP.iter()
7d3fb63208bfatagen2026-10-11 04:38 UTC167
                .filter_map(|item| {
7d3fb63208bfatagen2026-10-11 04:38 UTC168
                    let price = item.price()?;
7d3fb63208bfatagen2026-10-11 04:38 UTC169
                    Some(format!(
7d3fb63208bfatagen2026-10-11 04:38 UTC170
                        "<{}>; rel=\"{REL_BASE}shop-item\"; title=\"{} for {price}\"; allow=\"POST\"",
7d3fb63208bfatagen2026-10-11 04:38 UTC171
                        run.path(&format!("/shop/{}", item.slug())),
7d3fb63208bfatagen2026-10-11 04:38 UTC172
                        item.slug()
7d3fb63208bfatagen2026-10-11 04:38 UTC173
                    ))
7d3fb63208bfatagen2026-10-11 04:38 UTC174
                })
7d3fb63208bfatagen2026-10-11 04:38 UTC175
                .collect::<Vec<_>>()
7d3fb63208bfatagen2026-10-11 04:38 UTC176
                .join(", ")
7d3fb63208bfatagen2026-10-11 04:38 UTC177
        }))
185851447cebatagen2026-10-10 12:58 UTC178
        .collect::<Vec<_>>()
185851447cebatagen2026-10-10 12:58 UTC179
        .join(", ")
782069860236atagen2026-10-10 12:44 UTC180
}
782069860236atagen2026-10-10 12:44 UTC181
782069860236atagen2026-10-10 12:44 UTC182
/// Shame lives in a signed cookie so it follows the player across runs. Each response adds this
782069860236atagen2026-10-10 12:44 UTC183
/// run's new shame to whatever total the client presents, so two runs at once both count.
782069860236atagen2026-10-10 12:44 UTC184
fn settle_shame(run: &mut Run, ctx: &Ctx, secret: &Secret) -> u32 {
782069860236atagen2026-10-10 12:44 UTC185
    let presented = ctx
782069860236atagen2026-10-10 12:44 UTC186
        .cookie("shame")
782069860236atagen2026-10-10 12:44 UTC187
        .and_then(|signed| secret.verify("shame", signed))
782069860236atagen2026-10-10 12:44 UTC188
        .and_then(|total| total.parse::<u32>().ok());
782069860236atagen2026-10-10 12:44 UTC189
    let total = presented.unwrap_or(run.shame_total) + (run.shame - run.shame_reported);
782069860236atagen2026-10-10 12:44 UTC190
    run.shame_reported = run.shame;
782069860236atagen2026-10-10 12:44 UTC191
    run.shame_total = total;
782069860236atagen2026-10-10 12:44 UTC192
    total
782069860236atagen2026-10-10 12:44 UTC193
}
782069860236atagen2026-10-10 12:44 UTC194
782069860236atagen2026-10-10 12:44 UTC195
pub fn finish(run: &mut Run, ctx: &Ctx, secret: &Secret, mut response: Response) -> Response {
782069860236atagen2026-10-10 12:44 UTC196
    let status = response.status();
782069860236atagen2026-10-10 12:44 UTC197
    if status == StatusCode::NOT_MODIFIED {
782069860236atagen2026-10-10 12:44 UTC198
        return response;
782069860236atagen2026-10-10 12:44 UTC199
    }
782069860236atagen2026-10-10 12:44 UTC200
    let shame = settle_shame(run, ctx, secret);
782069860236atagen2026-10-10 12:44 UTC201
    let mut stats: Vec<(HeaderName, String)> = vec![
f39b10e20a0batagen2026-10-10 14:33 UTC202
        if run.items.ruler {
d46f9d0b953aatagen2026-10-10 14:41 UTC203
            (X_LENGTH, format!("{:.2}", run.current_length()))
f39b10e20a0batagen2026-10-10 14:33 UTC204
        } else {
f39b10e20a0batagen2026-10-10 14:33 UTC205
            (
f39b10e20a0batagen2026-10-10 14:33 UTC206
                X_SELF_REPORTED_LENGTH,
f39b10e20a0batagen2026-10-10 14:33 UTC207
                format!("{:.2}", run.self_reported()),
f39b10e20a0batagen2026-10-10 14:33 UTC208
            )
f39b10e20a0batagen2026-10-10 14:33 UTC209
        },
782069860236atagen2026-10-10 12:44 UTC210
        (X_GIRTH, format!("{:.2}", run.girth)),
782069860236atagen2026-10-10 12:44 UTC211
        (
782069860236atagen2026-10-10 12:44 UTC212
            X_FORESKIN,
782069860236atagen2026-10-10 12:44 UTC213
            run.foreskin.map_or("gone".into(), |f| format!("{f:.2}")),
782069860236atagen2026-10-10 12:44 UTC214
        ),
5dfebcc0ac43atagen2026-10-11 05:28 UTC215
        (X_FRICTION, format!("{}/{}", run.friction, FRICTION_MAX)),
782069860236atagen2026-10-10 12:44 UTC216
        (X_EQ, run.eq.to_string()),
782069860236atagen2026-10-10 12:44 UTC217
        (X_STAMINA, format!("{}/{}", run.stamina, run.max_stamina())),
782069860236atagen2026-10-10 12:44 UTC218
        (X_SUSPICION, format!("{}/{SUSPICION_LIMIT}", run.suspicion)),
782069860236atagen2026-10-10 12:44 UTC219
        (X_SHAME, shame.to_string()),
782069860236atagen2026-10-10 12:44 UTC220
        (X_CASH, run.cash.to_string()),
782069860236atagen2026-10-10 12:44 UTC221
        (X_FLOOR, run.floor.number().to_string()),
58e1fb3943e6atagen2026-10-10 13:30 UTC222
        (header::LINK, links(run, ctx.now)),
782069860236atagen2026-10-10 12:44 UTC223
    ];
782069860236atagen2026-10-10 12:44 UTC224
    if let Some(observer) = run.observed {
782069860236atagen2026-10-10 12:44 UTC225
        stats.push((X_OBSERVED_BY, observer.name().into()));
782069860236atagen2026-10-10 12:44 UTC226
    }
9683736a2c5eatagen2026-10-10 13:15 UTC227
    if run.mum.is_some() {
9683736a2c5eatagen2026-10-10 13:15 UTC228
        stats.push((header::VIA, "1.1 mum".into()));
9683736a2c5eatagen2026-10-10 13:15 UTC229
        stats.push((header::VARY, "Mum".into()));
782069860236atagen2026-10-10 12:44 UTC230
    }
782069860236atagen2026-10-10 12:44 UTC231
    // RFC 9111 obsoleted Warning, which is why nobody heeds it.
782069860236atagen2026-10-10 12:44 UTC232
    if run.conditions.pulled_muscle {
782069860236atagen2026-10-10 12:44 UTC233
        stats.push((header::WARNING, "199 - \"a muscle has been pulled\"".into()));
782069860236atagen2026-10-10 12:44 UTC234
    }
5dfebcc0ac43atagen2026-10-11 05:28 UTC235
    if run.conditions.friction_burn {
5dfebcc0ac43atagen2026-10-11 05:28 UTC236
        stats.push((header::WARNING, "199 - \"friction burn\"".into()));
5dfebcc0ac43atagen2026-10-11 05:28 UTC237
    }
d46f9d0b953aatagen2026-10-10 14:41 UTC238
    for notice in run.notices.drain(..) {
d46f9d0b953aatagen2026-10-10 14:41 UTC239
        stats.push((header::WARNING, format!("199 - \"{notice}\"")));
d46f9d0b953aatagen2026-10-10 14:41 UTC240
    }
782069860236atagen2026-10-10 12:44 UTC241
    if run.eq > 70 && run.ended.is_none() {
782069860236atagen2026-10-10 12:44 UTC242
        stats.push((header::WARNING, "199 - \"it is turning purple\"".into()));
782069860236atagen2026-10-10 12:44 UTC243
    }
782069860236atagen2026-10-10 12:44 UTC244
    stats.push((
782069860236atagen2026-10-10 12:44 UTC245
        header::SET_COOKIE,
782069860236atagen2026-10-10 12:44 UTC246
        format!(
782069860236atagen2026-10-10 12:44 UTC247
            "shame={}; {COOKIE_ATTRIBUTES}",
782069860236atagen2026-10-10 12:44 UTC248
            secret.sign("shame", &shame.to_string())
782069860236atagen2026-10-10 12:44 UTC249
        ),
782069860236atagen2026-10-10 12:44 UTC250
    ));
782069860236atagen2026-10-10 12:44 UTC251
    if run.items.ruler {
782069860236atagen2026-10-10 12:44 UTC252
        stats.push((
782069860236atagen2026-10-10 12:44 UTC253
            header::SET_COOKIE,
782069860236atagen2026-10-10 12:44 UTC254
            format!("ruler={}; {COOKIE_ATTRIBUTES}", secret.sign("ruler", "1")),
782069860236atagen2026-10-10 12:44 UTC255
        ));
782069860236atagen2026-10-10 12:44 UTC256
    }
782069860236atagen2026-10-10 12:44 UTC257
    let headers = response.headers_mut();
782069860236atagen2026-10-10 12:44 UTC258
    for (name, value) in stats {
782069860236atagen2026-10-10 12:44 UTC259
        if let Ok(value) = HeaderValue::from_str(&value) {
782069860236atagen2026-10-10 12:44 UTC260
            headers.append(name, value);
782069860236atagen2026-10-10 12:44 UTC261
        }
782069860236atagen2026-10-10 12:44 UTC262
    }
782069860236atagen2026-10-10 12:44 UTC263
    let bodiless = status.is_informational() || status == StatusCode::NO_CONTENT;
782069860236atagen2026-10-10 12:44 UTC264
    if run.items.ring && !bodiless {
782069860236atagen2026-10-10 12:44 UTC265
        response.extensions_mut().insert(Compress);
782069860236atagen2026-10-10 12:44 UTC266
    }
782069860236atagen2026-10-10 12:44 UTC267
    response
782069860236atagen2026-10-10 12:44 UTC268
}
782069860236atagen2026-10-10 12:44 UTC269
782069860236atagen2026-10-10 12:44 UTC270
fn greet(response: Response, ctx: &mut Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC271
    if response.status() != StatusCode::SWITCHING_PROTOCOLS {
782069860236atagen2026-10-10 12:44 UTC272
        return response;
782069860236atagen2026-10-10 12:44 UTC273
    }
782069860236atagen2026-10-10 12:44 UTC274
    let (Some(Greeting(text)), Some(upgrade)) = (
782069860236atagen2026-10-10 12:44 UTC275
        response.extensions().get::<Greeting>().cloned(),
782069860236atagen2026-10-10 12:44 UTC276
        ctx.upgrade.take(),
782069860236atagen2026-10-10 12:44 UTC277
    ) else {
782069860236atagen2026-10-10 12:44 UTC278
        return response;
782069860236atagen2026-10-10 12:44 UTC279
    };
782069860236atagen2026-10-10 12:44 UTC280
    tokio::spawn(async move {
782069860236atagen2026-10-10 12:44 UTC281
        if let Ok(upgraded) = upgrade.await {
782069860236atagen2026-10-10 12:44 UTC282
            let mut io = TokioIo::new(upgraded);
782069860236atagen2026-10-10 12:44 UTC283
            let _ = io.write_all(text.as_bytes()).await;
782069860236atagen2026-10-10 12:44 UTC284
            let _ = io.shutdown().await;
782069860236atagen2026-10-10 12:44 UTC285
        }
782069860236atagen2026-10-10 12:44 UTC286
    });
782069860236atagen2026-10-10 12:44 UTC287
    response
782069860236atagen2026-10-10 12:44 UTC288
}