atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
ea51d9fda49d82c38cf3920e34d6be05d8398cae
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/meta.rsRust145 lines4.3 KB
1
//! Resources that do not belong to a single run.
2
3
use axum::{
4
    Json,
5
    http::{StatusCode, header},
6
    response::{IntoResponse, Response},
7
};
8
use rand::Rng;
9
use serde_json::json;
10
11
use super::{
12
    Shared,
13
    ctx::Ctx,
14
    leaderboard::{graveyard, leaderboard},
15
    resource::{Resource, Verb},
16
    run::{self, records_unavailable},
17
};
18
use crate::{
19
    game::{ELDER_LAST_MODIFIED, FORUM_THREAD, Run, forum_thread},
20
    headers::X_SEED,
21
    problem::Problem,
22
};
23
24
pub static INDEX: Resource<Shared> = Resource::new(&[Verb::new("GET", index)]);
25
pub static RUNS: Resource<Shared> = Resource::new(&[Verb::new("POST", create_run)]);
26
pub static THREAD: Resource<Shared> = Resource::new(&[Verb::new("GET", thread)]);
27
pub const GRAVEYARD_PATH: &str = "/graveyard";
28
pub const LEADERBOARD_PATH: &str = "/leaderboard";
29
30
pub static GRAVEYARD: Resource<Shared> = Resource::new(&[Verb::new("GET", graveyard)]);
31
pub static LEADERBOARD: Resource<Shared> = Resource::new(&[Verb::new("GET", leaderboard)]);
32
33
fn index(_: &mut Shared, ctx: &Ctx) -> Response {
34
    let wants_html = ctx
35
        .header(header::ACCEPT)
36
        .is_some_and(|accept| accept.contains("text/html"));
37
    if wants_html {
38
        return (
39
            StatusCode::SEE_OTHER,
40
            [(header::LOCATION, "/ui/"), (header::VARY, "Accept")],
41
        )
42
            .into_response();
43
    }
44
    (
45
        [(header::VARY, "Accept")],
46
        Json(json!({
47
            "detail": "This is jelqd. It is a roguelike. It is played over HTTP.",
48
            "start": { "href": "/runs", "method": "POST" },
49
            "leaderboard": LEADERBOARD_PATH,
50
            "graveyard": GRAVEYARD_PATH,
51
            "forum": FORUM_THREAD,
52
            "ui": "/ui/",
53
        })),
54
    )
55
        .into_response()
56
}
57
58
fn create_run(state: &mut Shared, ctx: &Ctx) -> Response {
59
    let seeded = ctx.query("seed").is_some();
60
    let seed = match ctx.query("seed").map(str::parse::<u64>) {
61
        None => rand::rng().random(),
62
        Some(Ok(seed)) => seed,
63
        Some(Err(_)) => {
64
            return Problem::new(
65
                StatusCode::BAD_REQUEST,
66
                "bad-seed",
67
                "The seed must be an unsigned 64-bit integer.",
68
            )
69
            .into_response();
70
        }
71
    };
72
    let config = &state.config;
73
    if state.runs.len() >= config.max_runs {
74
        return Problem::new(
75
            StatusCode::SERVICE_UNAVAILABLE,
76
            "server-full",
77
            "The server is full. Everyone is in there.",
78
        )
79
        .header(header::RETRY_AFTER, "600")
80
        .into_response();
81
    }
82
    let mut run = loop {
83
        let id = format!("{:032x}", rand::rng().random::<u128>());
84
        match state.records.find(&id) {
85
            Ok(None) => {}
86
            Ok(Some(_)) => continue,
87
            Err(error) => return records_unavailable(&error),
88
        }
89
        break Run::with_oracle(
90
            id,
91
            format!("{:08x}", rand::rng().random::<u32>()),
92
            config.secret.mix_seed(seed),
93
            seeded,
94
            &config.oracle,
95
        );
96
    };
97
    let response = (
98
        StatusCode::CREATED,
99
        [
100
            (header::LOCATION, run.path("")),
101
            (header::ETAG, run.etag()),
102
            (X_SEED, seed.to_string()),
103
        ],
104
        Json(run.view()),
105
    )
106
        .into_response();
107
    let response = run::finish(&mut run, ctx, &config.secret, response);
108
    if state.runs.insert_new(run) {
109
        response
110
    } else {
111
        create_run(state, ctx)
112
    }
113
}
114
115
fn thread(_: &mut Shared, ctx: &Ctx) -> Response {
116
    match ctx.param("year").parse::<u32>() {
117
        Ok(2009) => (
118
            [(header::LAST_MODIFIED, ELDER_LAST_MODIFIED)],
119
            Json(forum_thread()),
120
        )
121
            .into_response(),
122
        Ok(year) if year > 2009 => Problem::new(
123
            StatusCode::NOT_FOUND,
124
            "no-thread",
125
            "Nobody has posted since 2009.",
126
        )
127
        .with("thread", FORUM_THREAD)
128
        .into_response(),
129
        _ => Problem::new(
130
            StatusCode::NOT_FOUND,
131
            "no-thread",
132
            "The forum did not exist yet. The problem did.",
133
        )
134
        .with("thread", FORUM_THREAD)
135
        .into_response(),
136
    }
137
}
138
139
pub async fn not_found() -> Problem {
140
    Problem::new(
141
        StatusCode::NOT_FOUND,
142
        "not-found",
143
        "The resource does not exist. This is not about you.",
144
    )
145
}