atagen

jelq

public

an http based roguelike rpg about jelqing

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