atagen

jelq

public

an http based roguelike rpg about jelqing

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