atagen

jelq

public

an http based roguelike rpg about jelqing

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