atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
a8269407e054211fed82e710616e72cfa4f72961
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/runs.rsRust329 lines9.9 KB
1
//! The resources under `/runs/{id}`.
2
3
use axum::{
4
    Json,
5
    http::{StatusCode, header},
6
    response::{IntoResponse, Response},
7
};
8
use serde_json::{Value, json};
9
10
use super::{
11
    ctx::Ctx,
12
    inventory::{delete_item, get_inventory, get_item},
13
    parse::{parse_intensity, parse_techniques},
14
    resource::{Resource, Verb},
15
    response::respond,
16
    run::{Greeting, RunResource, Scope},
17
    shaft::{get_foreskin, get_length},
18
};
19
use crate::{
20
    game::{
21
        Branch, Cause, Floor, Grip, Item, LUBRICANTS, Lube, PatchOp, Run, Technique,
22
        balance::{EQ_BAND, VIEWS_PER_SECOND},
23
    },
24
    headers::ACCEPT_PATCH,
25
    problem::Problem,
26
};
27
28
const fn home(verbs: &'static [Verb<Run>]) -> RunResource {
29
    RunResource {
30
        scope: Scope::Home,
31
        resource: Resource::new(verbs),
32
    }
33
}
34
35
const fn pockets(verbs: &'static [Verb<Run>]) -> RunResource {
36
    RunResource {
37
        scope: Scope::Pockets,
38
        resource: Resource::new(verbs),
39
    }
40
}
41
42
fn not_in_clinic(run: &Run) -> bool {
43
    run.floor != Floor::Clinic
44
}
45
46
pub static RUN: RunResource = RunResource {
47
    scope: Scope::Home,
48
    resource: Resource::patchable(&[
49
        Verb::new("GET", get_run),
50
        Verb::new("JELQ", jelq).only_when(not_in_clinic, "JELQ is not performed in the clinic."),
51
        Verb::new("HOLD", hold_run).only_when(
52
            Run::can_hold,
53
            "HOLD is only available above 70 EQ or at 2 stamina or less.",
54
        ),
55
        Verb::new("PUT", put_run),
56
        Verb::new("PATCH", patch_run),
57
        Verb::new("DELETE", delete_run),
58
        Verb::new("TRACE", trace_run),
59
    ]),
60
};
61
pub static LENGTH: RunResource = home(&[Verb::new("GET", get_length)]);
62
pub static FORESKIN: RunResource = home(&[
63
    Verb::new("GET", get_foreskin),
64
    Verb::new("DELETE", delete_foreskin),
65
]);
66
pub static INVENTORY: RunResource = pockets(&[Verb::new("GET", get_inventory)]);
67
pub static ITEM: RunResource =
68
    pockets(&[Verb::new("GET", get_item), Verb::new("DELETE", delete_item)]);
69
pub static TECHNIQUES: RunResource = home(&[Verb::new("GET", get_techniques)]);
70
pub static DRAWER: RunResource = home(&[Verb::new("POST", search_drawer)]);
71
pub static BRANCH: RunResource = home(&[Verb::new("POST", choose_branch)]);
72
pub static KITCHEN: RunResource = home(&[Verb::new("POST", raid_kitchen)]);
73
pub static MELON: RunResource = home(&[Verb::new("POST", commit_melon)]);
74
pub static THREAD: RunResource = pockets(&[Verb::new("GET", get_thread)]);
75
pub static FLOOR: RunResource =
76
    home(&[Verb::new("GET", get_floor), Verb::new("POST", leave_floor)]);
77
pub static SHOP: RunResource = home(&[Verb::new("GET", get_shop)]);
78
pub static BUY: RunResource = home(&[Verb::new("POST", buy)]);
79
pub static PORTAL: RunResource = pockets(&[
80
    Verb::new("GET", get_portal),
81
    Verb::new("POST", accept_portal),
82
]);
83
pub static ELDER: RunResource = home(&[Verb::new("GET", get_elder)]);
84
pub static GRIP: RunResource = home(&[Verb::new("GET", get_grip)]);
85
pub static DIAGNOSIS: RunResource = pockets(&[Verb::new("GET", get_diagnosis)]);
86
pub static FOOTAGE: RunResource = pockets(&[
87
    Verb::new("GET", get_footage),
88
    Verb::refused(
89
        "DELETE",
90
        "The footage has been cached by intermediaries. It cannot be deleted.",
91
    ),
92
]);
93
94
fn get_run(run: &mut Run, ctx: &Ctx) -> Response {
95
    if ctx
96
        .header(header::ACCEPT)
97
        .is_some_and(|accept| accept.contains("reassurance"))
98
    {
99
        return Problem::new(
100
            StatusCode::NOT_ACCEPTABLE,
101
            "no-reassurance",
102
            "The server does not produce reassurance.",
103
        )
104
        .into_response();
105
    }
106
    (
107
        StatusCode::OK,
108
        [(header::ETAG, run.etag())],
109
        Json(run.view()),
110
    )
111
        .into_response()
112
}
113
114
fn jelq(run: &mut Run, ctx: &Ctx) -> Response {
115
    let parsed = parse_techniques(run, ctx).and_then(|techniques| {
116
        let intensity = parse_intensity(ctx)?;
117
        let lube = Lube::from_content_type(ctx.header(header::CONTENT_TYPE))?;
118
        Ok((techniques, intensity, lube))
119
    });
120
    match parsed {
121
        Ok((techniques, intensity, lube)) => respond(run.jelq(&techniques, intensity, lube)),
122
        Err(problem) => problem.into_response(),
123
    }
124
}
125
126
fn put_run(run: &mut Run, _: &Ctx) -> Response {
127
    run.put().into_response()
128
}
129
130
fn hold_run(run: &mut Run, _: &Ctx) -> Response {
131
    respond(run.hold())
132
}
133
134
fn patch_run(run: &mut Run, ctx: &Ctx) -> Response {
135
    let is_patch = ctx
136
        .header(header::CONTENT_TYPE)
137
        .is_some_and(|ct| ct.starts_with("application/json-patch+json"));
138
    if !is_patch {
139
        return Problem::new(
140
            StatusCode::UNSUPPORTED_MEDIA_TYPE,
141
            "patch-format",
142
            "Patches are accepted as application/json-patch+json.",
143
        )
144
        .header(ACCEPT_PATCH, "application/json-patch+json")
145
        .into_response();
146
    }
147
    match serde_json::from_slice::<Vec<PatchOp>>(ctx.body()) {
148
        Ok(ops) => respond(run.patch(&ops)),
149
        Err(_) => Problem::new(
150
            StatusCode::BAD_REQUEST,
151
            "malformed-patch",
152
            "The patch could not be parsed.",
153
        )
154
        .into_response(),
155
    }
156
}
157
158
fn delete_run(run: &mut Run, _: &Ctx) -> Response {
159
    run.end(Cause::Abandoned);
160
    StatusCode::NO_CONTENT.into_response()
161
}
162
163
fn trace_run(run: &mut Run, ctx: &Ctx) -> Response {
164
    run.trace();
165
    (
166
        StatusCode::OK,
167
        [(header::CONTENT_TYPE, "message/http")],
168
        ctx.echo(),
169
    )
170
        .into_response()
171
}
172
173
fn delete_foreskin(run: &mut Run, ctx: &Ctx) -> Response {
174
    respond(run.delete_foreskin(ctx.header(header::IF_MATCH)))
175
}
176
177
fn get_techniques(run: &mut Run, _: &Ctx) -> Response {
178
    let listed: Vec<Value> = Technique::ALL
179
        .into_iter()
180
        .map(|t| t.describe(run.knows(t)))
181
        .collect();
182
    Json(json!({
183
        "usage": format!("JELQ {}?technique=<name>[,<name>]&intensity=<1-10>", run.path("")),
184
        "lubricant": "Declared with Content-Type.",
185
        "lubricants": LUBRICANTS,
186
        "eq_band": [EQ_BAND.start(), EQ_BAND.end()],
187
        "techniques": listed,
188
    }))
189
    .into_response()
190
}
191
192
fn search_drawer(run: &mut Run, _: &Ctx) -> Response {
193
    respond(run.search_drawer())
194
}
195
196
fn choose_branch(run: &mut Run, ctx: &Ctx) -> Response {
197
    match ctx.param("choice").parse::<Branch>() {
198
        Ok(choice) => respond(run.choose(choice)),
199
        Err(()) => Problem::new(
200
            StatusCode::NOT_FOUND,
201
            "no-such-choice",
202
            "That was not one of the choices.",
203
        )
204
        .into_response(),
205
    }
206
}
207
208
fn get_floor(run: &mut Run, _: &Ctx) -> Response {
209
    Json(run.floor_view()).into_response()
210
}
211
212
fn leave_floor(run: &mut Run, _: &Ctx) -> Response {
213
    respond(run.advance())
214
}
215
216
fn get_shop(run: &mut Run, _: &Ctx) -> Response {
217
    respond(run.browse_shop().map(Json))
218
}
219
220
fn buy(run: &mut Run, ctx: &Ctx) -> Response {
221
    respond(run.buy(Item::parse(ctx.param("item"))))
222
}
223
224
fn raid_kitchen(run: &mut Run, _: &Ctx) -> Response {
225
    respond(run.raid_kitchen())
226
}
227
228
fn commit_melon(run: &mut Run, _: &Ctx) -> Response {
229
    respond(run.commit_melon())
230
}
231
232
fn get_thread(run: &mut Run, _: &Ctx) -> Response {
233
    respond(run.read_thread())
234
}
235
236
fn get_portal(run: &mut Run, _: &Ctx) -> Response {
237
    Json(json!({
238
        "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.",
239
        "accept": { "href": run.path("/portal"), "method": "POST" },
240
    }))
241
    .into_response()
242
}
243
244
fn accept_portal(run: &mut Run, _: &Ctx) -> Response {
245
    respond(run.accept_terms())
246
}
247
248
fn get_elder(run: &mut Run, ctx: &Ctx) -> Response {
249
    respond(run.elder(
250
        ctx.header(header::PROXY_AUTHORIZATION),
251
        ctx.header(header::IF_MODIFIED_SINCE),
252
    ))
253
}
254
255
fn requested_grip(ctx: &Ctx) -> Option<Grip> {
256
    let connection = ctx.header(header::CONNECTION)?.to_ascii_lowercase();
257
    if !connection.split(',').any(|token| token.trim() == "upgrade") {
258
        return None;
259
    }
260
    ctx.header(header::UPGRADE)?
261
        .split(',')
262
        .find_map(|protocol| protocol.trim().parse::<Grip>().ok())
263
}
264
265
fn get_grip(run: &mut Run, ctx: &Ctx) -> Response {
266
    if let Some(grip) = requested_grip(ctx) {
267
        if let Err(problem) = run.set_grip(grip) {
268
            return problem.into_response();
269
        }
270
        let mut response = (
271
            StatusCode::SWITCHING_PROTOCOLS,
272
            [
273
                (header::UPGRADE, grip.name()),
274
                (header::CONNECTION, "upgrade"),
275
            ],
276
        )
277
            .into_response();
278
        response.extensions_mut().insert(Greeting(format!(
279
            "You are now jelqing {}.\r\n",
280
            grip.name()
281
        )));
282
        return response;
283
    }
284
    let detail = match ctx.header(header::UPGRADE) {
285
        Some(protocol) => {
286
            format!("The server does not know the {protocol} grip. Grips are protocols.")
287
        }
288
        None => "Grips are protocols. Change grip by upgrading the connection.".to_owned(),
289
    };
290
    (
291
        StatusCode::OK,
292
        [(header::UPGRADE, "overhand, underhand")],
293
        Json(json!({ "grip": run.grip().name(), "detail": detail })),
294
    )
295
        .into_response()
296
}
297
298
fn get_diagnosis(run: &mut Run, ctx: &Ctx) -> Response {
299
    respond(run.diagnose(
300
        ctx.header(header::AUTHORIZATION),
301
        ctx.header(header::IF_NONE_MATCH),
302
    ))
303
}
304
305
fn get_footage(run: &mut Run, _: &Ctx) -> Response {
306
    let Some((filmed, age)) = run.footage() else {
307
        return Problem::new(
308
            StatusCode::NOT_FOUND,
309
            "no-footage",
310
            "There is no footage. Yet.",
311
        )
312
        .into_response();
313
    };
314
    (
315
        [
316
            (
317
                header::CACHE_CONTROL,
318
                "public, max-age=31536000, immutable".to_owned(),
319
            ),
320
            (header::AGE, age.to_string()),
321
            (header::LAST_MODIFIED, httpdate::fmt_http_date(filmed)),
322
        ],
323
        Json(json!({
324
            "detail": "A recording. It is grainy. It is unmistakably you. It has been shared.",
325
            "views": 1 + age * VIEWS_PER_SECOND,
326
        })),
327
    )
328
        .into_response()
329
}