atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
f16ab17c8ff6e8e90a8d6658a1fa79039509d7d7
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/run.rsRust222 lines7.1 KB
1
//! Plumbing shared by every resource under `/runs/{id}`.
2
3
use axum::{
4
    http::{HeaderName, HeaderValue, StatusCode, header},
5
    response::{IntoResponse, Response},
6
};
7
use hyper_util::rt::TokioIo;
8
use tokio::io::AsyncWriteExt;
9
10
use super::{
11
    AppState, ctx::Ctx, links, middleware::Compress, registry::Lock, resource::Resource, shame,
12
};
13
use crate::{
14
    game::{
15
        Cause, Gauges, Run, Shown,
16
        balance::{FRICTION_MAX, SUSPICION_LIMIT},
17
    },
18
    headers::{
19
        X_CASH, X_EQ, X_FLOOR, X_FORESKIN, X_FRICTION, X_GIRTH, X_LENGTH, X_OBSERVED_BY,
20
        X_SELF_REPORTED_LENGTH, X_SHAME, X_STAMINA, X_SUSPICION,
21
    },
22
    locale::Locale,
23
    problem::{PROBLEM_BASE, Problem},
24
    records::Record,
25
    secret::Secret,
26
};
27
28
/// Which household hazards stand between a request and the run.
29
#[derive(Clone, Copy, PartialEq, Eq)]
30
pub enum Scope {
31
    Home,
32
    Pockets,
33
}
34
35
pub struct RunResource {
36
    pub scope: Scope,
37
    pub resource: Resource<Run>,
38
}
39
40
/// What the server writes over the connection after a `101`.
41
#[derive(Clone)]
42
pub struct Greeting(pub String);
43
44
pub fn records_unavailable(error: &rusqlite::Error) -> Response {
45
    Problem::new(
46
        StatusCode::SERVICE_UNAVAILABLE,
47
        "records-unavailable",
48
        "The records are unavailable. Nothing has been forgotten, it is only out of reach.",
49
    )
50
    .with("error", error.to_string())
51
    .into_response()
52
}
53
54
pub fn serve(state: &AppState, mut ctx: Ctx, target: &RunResource) -> Response {
55
    let id = ctx.param("id").to_owned();
56
    let Some(slot) = state.runs.get(&id) else {
57
        return match state.records.find(&id) {
58
            Ok(Some(Record {
59
                cause: Some(cause),
60
                turn,
61
                ..
62
            })) => gone(cause.slug(), turn),
63
            Ok(_) => Problem::new(
64
                StatusCode::NOT_FOUND,
65
                "no-such-run",
66
                "There is no such run. There may never have been.",
67
            )
68
            .into_response(),
69
            Err(error) => records_unavailable(&error),
70
        };
71
    };
72
    let (response, record) = match Lock::take(&slot) {
73
        Lock::Poisoned(mut run) => {
74
            run.lose();
75
            let response = gone(Cause::Lost.slug(), run.turn);
76
            (response, Some(Record::of(&run)))
77
        }
78
        Lock::Held(mut run) => {
79
            if let Some(ending) = &run.ended {
80
                return gone(ending.cause.slug(), ending.turn);
81
            }
82
            run.seen(ctx.now());
83
            let response = gate(&mut run, &ctx, target.scope)
84
                .unwrap_or_else(|| target.resource.dispatch(&mut run, &ctx));
85
            let response = finish(&mut run, &ctx, &state.config.secret, response);
86
            let record = run.status().finished().then(|| Record::of(&run));
87
            (response, record)
88
        }
89
    };
90
    if let Some(record) = record {
91
        if let Err(error) = state.records.save(&record) {
92
            return records_unavailable(&error);
93
        }
94
        if record.cause.is_some() {
95
            state.runs.remove(&id);
96
        }
97
    }
98
    greet(response, &mut ctx)
99
}
100
101
fn gone(cause: &str, turn: u32) -> Response {
102
    Problem::new(StatusCode::GONE, "run-ended", "This run has ended.")
103
        .with("cause", format!("{PROBLEM_BASE}{cause}"))
104
        .with("turn", turn)
105
        .with("graveyard", "/graveyard")
106
        .into_response()
107
}
108
109
fn gate(run: &mut Run, ctx: &Ctx, scope: Scope) -> Option<Response> {
110
    if scope == Scope::Pockets {
111
        return None;
112
    }
113
    if run.needs_portal() {
114
        let portal = run.path("/portal");
115
        return Some(
116
            Problem::new(
117
                StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
118
                "captive-portal",
119
                "The network requires authentication. The hotel would like you to agree to some terms.",
120
            )
121
            .with("portal", portal.as_str())
122
            .header(
123
                header::LINK,
124
                format!("<{portal}>; rel=\"{}portal\"", links::REL_BASE),
125
            )
126
            .into_response(),
127
        );
128
    }
129
    let authorization = ctx
130
        .header(header::AUTHORIZATION)
131
        .map(|text| Locale::negotiate(ctx.header(header::ACCEPT_LANGUAGE)).canonicalise(text));
132
    run.answer_door(authorization.as_deref())
133
        .err()
134
        .map(IntoResponse::into_response)
135
}
136
137
fn stats(gauges: &Gauges, shame: u32) -> Vec<(HeaderName, String)> {
138
    let mut stats = vec![
139
        match gauges.length {
140
            Shown::Measured(length) => (X_LENGTH, format!("{length:.2}")),
141
            Shown::SelfReported(length) => (X_SELF_REPORTED_LENGTH, format!("{length:.2}")),
142
        },
143
        (X_GIRTH, format!("{:.2}", gauges.girth)),
144
        (
145
            X_FORESKIN,
146
            gauges.foreskin.map_or("gone".into(), |f| format!("{f:.2}")),
147
        ),
148
        (X_FRICTION, format!("{}/{FRICTION_MAX}", gauges.friction)),
149
        (X_EQ, gauges.eq.to_string()),
150
        (
151
            X_STAMINA,
152
            format!("{}/{}", gauges.stamina, gauges.max_stamina),
153
        ),
154
        (
155
            X_SUSPICION,
156
            format!("{}/{SUSPICION_LIMIT}", gauges.suspicion),
157
        ),
158
        (X_SHAME, shame.to_string()),
159
        (X_CASH, gauges.cash.to_string()),
160
        (X_FLOOR, gauges.floor.to_string()),
161
    ];
162
    if let Some(observer) = gauges.observed {
163
        stats.push((X_OBSERVED_BY, observer.name().into()));
164
    }
165
    if gauges.mum_at_door {
166
        stats.push((header::VIA, "1.1 mum".into()));
167
        stats.push((header::VARY, "Mum".into()));
168
    }
169
    // RFC 9111 obsoleted Warning, which is why nobody heeds it.
170
    for warning in &gauges.warnings {
171
        stats.push((header::WARNING, format!("199 - \"{warning}\"")));
172
    }
173
    stats
174
}
175
176
pub fn finish(run: &mut Run, ctx: &Ctx, secret: &Secret, mut response: Response) -> Response {
177
    let status = response.status();
178
    if status == StatusCode::NOT_MODIFIED {
179
        return response;
180
    }
181
    let (shame, shame_cookie) = shame::settle(run, ctx, secret);
182
    let gauges = run.gauges();
183
    let link = links::header(run);
184
    let headers = response.headers_mut();
185
    for (name, value) in stats(&gauges, shame) {
186
        if let Ok(value) = HeaderValue::from_str(&value) {
187
            headers.append(name, value);
188
        }
189
    }
190
    if let Ok(link) = HeaderValue::from_str(&link) {
191
        headers.append(header::LINK, link);
192
    }
193
    headers.append(header::SET_COOKIE, shame_cookie);
194
    if gauges.ruler {
195
        headers.append(header::SET_COOKIE, shame::ruler_cookie(secret));
196
    }
197
    let bodiless = status.is_informational() || status == StatusCode::NO_CONTENT;
198
    if gauges.ring && !bodiless {
199
        response.extensions_mut().insert(Compress);
200
    }
201
    response
202
}
203
204
fn greet(response: Response, ctx: &mut Ctx) -> Response {
205
    if response.status() != StatusCode::SWITCHING_PROTOCOLS {
206
        return response;
207
    }
208
    let (Some(Greeting(text)), Some(upgrade)) = (
209
        response.extensions().get::<Greeting>().cloned(),
210
        ctx.take_upgrade(),
211
    ) else {
212
        return response;
213
    };
214
    tokio::spawn(async move {
215
        if let Ok(upgraded) = upgrade.await {
216
            let mut io = TokioIo::new(upgraded);
217
            let _ = io.write_all(text.as_bytes()).await;
218
            let _ = io.shutdown().await;
219
        }
220
    });
221
    response
222
}