atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
e807266c323bcc1fd00f59aa160b3b94d8168beb
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/forum.rsRust233 lines8.9 KB
1
use axum::http::{StatusCode, header};
2
use serde_json::{Value, json};
3
4
use super::{
5
    Cause, Elder, Floor, Outcome, Run, Story,
6
    balance::{EXAGGERATION, PATIENCE, SUSPICION_LIMIT, ULI_ODDS},
7
    body::exaggeration,
8
    honest, parse_credentials,
9
};
10
use crate::{headers::X_FORUM_NOTE, problem::Problem};
11
12
pub const FORUM_THREAD: &str = "/forum/threads/2009";
13
pub const ELDER_LAST_MODIFIED: &str = "Sun, 14 Jun 2009 03:12:00 GMT";
14
15
const SPREADSHEET: &str = "\
16
date,length,girth,technique,notes,honest?
17
2007-01-03,7.50,5.50,wet,baseline,no
18
2007-06-11,7.50,5.50,wet,feeling fuller,no
19
2008-01-02,7.50,5.50,dry,new routine,no
20
2008-09-19,7.50,5.50,uli,PUT is idempotent. do not use PUT.,no
21
2009-06-14,7.50,5.50,wet,progress is slow but real,no
22
";
23
24
pub fn forum_thread() -> Value {
25
    json!({
26
        "title": "The definitive jelqing FAQ (updated 2009)",
27
        "posts": [
28
            { "author": "TheElder", "body": "Warm up. Always warm up. A cold jelq is a 412 waiting to happen." },
29
            { "author": "TheElder", "body": "Keep EQ between 50 and 70. Above 70 you are gambling. Above 90 you are a cautionary tale." },
30
            { "author": "newbie_2009", "body": "everyone keeps saying uli. who is uli" },
31
            { "author": "TheElder", "body": "uli was a member here. He posted the technique in 2001 and never posted again. Some say he ascended. The search function says his account is gone." },
32
            { "author": "TheElder", "body": "We do not say his name in vain. We say it in girth." },
33
            { "author": "newbie_2009", "body": "I have been sending PUT for three weeks and nothing has changed." },
34
            { "author": "TheElder", "body": "PUT is idempotent. Read the spec." },
35
            { "author": "TheElder", "body": "Members verify with a Ruler. Proxy-Authorization, inches, no rounding up. We can tell." },
36
            { "author": "throwaway_77", "body": "my doctor said i was normal. i didnt believe him. i still dont know what he meant by bearer" },
37
            { "author": "TheElder", "body": "My numbers are in my spreadsheet. They have not changed since 2009. Nothing about me has changed since 2009." },
38
        ],
39
    })
40
}
41
42
fn verification(detail: &str) -> Problem {
43
    Problem::new(
44
        StatusCode::PROXY_AUTHENTICATION_REQUIRED,
45
        "verification-required",
46
        detail,
47
    )
48
    .header(header::PROXY_AUTHENTICATE, "Ruler realm=\"forum\"")
49
}
50
51
impl Run {
52
    pub(super) fn branch_offer(&self) -> Outcome {
53
        let forum = self.path("/branches/forum");
54
        let close = self.path("/branches/close-tab");
55
        Outcome::json(
56
            StatusCode::MULTIPLE_CHOICES,
57
            json!({
58
                "detail": "A browser tab is open. It has been open for some time.",
59
                "choices": [
60
                    { "href": forum, "method": "POST", "title": "Read the forum" },
61
                    { "href": close, "method": "POST", "title": "Close the tab" },
62
                ],
63
            }),
64
        )
65
        .header(
66
            header::LINK,
67
            format!(
68
                "<{forum}>; rel=\"alternate\"; title=\"Read the forum\"; allow=\"POST\", <{close}>; rel=\"alternate\"; title=\"Close the tab\"; allow=\"POST\""
69
            ),
70
        )
71
    }
72
73
    pub fn choose(&mut self, choice: &str) -> Result<Outcome, Problem> {
74
        if self.story != Story::Pending {
75
            return Err(Problem::new(
76
                StatusCode::CONFLICT,
77
                "no-decision-pending",
78
                "There is no decision pending.",
79
            ));
80
        }
81
        let (story, location, detail) = match choice {
82
            "forum" => {
83
                self.shame += 3;
84
                let detail =
85
                    "The thread is from 2009. A technique called uli is discussed at length.";
86
                (Story::ReadForum, self.path("/techniques"), detail)
87
            }
88
            "close-tab" => {
89
                self.suspicion = 0;
90
                (
91
                    Story::ClosedTab,
92
                    self.path(""),
93
                    "The tab is closed. Nobody saw it.",
94
                )
95
            }
96
            _ => {
97
                return Err(Problem::new(
98
                    StatusCode::NOT_FOUND,
99
                    "no-such-choice",
100
                    "That was not one of the choices.",
101
                ));
102
            }
103
        };
104
        self.story = story;
105
        self.touch();
106
        Ok(
107
            Outcome::json(StatusCode::SEE_OTHER, json!({ "detail": detail }))
108
                .header(header::LOCATION, location),
109
        )
110
    }
111
112
    /// Opening the thread again costs the turn, and each read may finally surface the reply.
113
    pub fn read_thread(&mut self) -> Result<Outcome, Problem> {
114
        if self.story == Story::Pending {
115
            return Ok(self.branch_offer());
116
        }
117
        if !self.thread_reachable() {
118
            return Err(Problem::new(
119
                StatusCode::NOT_FOUND,
120
                "no-thread",
121
                "There is no thread to re-read. Nobody has mentioned a forum yet.",
122
            ));
123
        }
124
        self.touch();
125
        self.turn += 1;
126
        self.here.turns += 1;
127
        self.end_kitchen_visit();
128
        self.realise();
129
        if self.chance(
130
            self.decision(
131
                "rupture",
132
                "Does the third read of the same thread rupture something?",
133
            ),
134
            self.rupture_odds(),
135
        ) {
136
            return Err(self.rupture());
137
        }
138
        if self.chance(
139
            self.decision(
140
                "teach-uli",
141
                "Does this reread finally surface the 2009 reply that teaches uli?",
142
            ),
143
            ULI_ODDS,
144
        ) {
145
            self.uli_taught = true;
146
            return Ok(Outcome::json(
147
                StatusCode::OK,
148
                json!({ "detail": "Past the necroposting, a reply from TheElder lays out the technique. Uli. The full routine. You write it down before the tab can lose it." }),
149
            )
150
            .header(header::LOCATION, self.path("/techniques")));
151
        }
152
        let was_below = self.suspicion < SUSPICION_LIMIT;
153
        self.settle(was_below);
154
        Ok(Outcome::empty(StatusCode::NOT_MODIFIED).header(
155
            header::WARNING,
156
            r#"199 - "Nothing new has been posted since 2009.""#,
157
        ))
158
    }
159
160
    fn elder_strike(&mut self, problem: Problem) -> Problem {
161
        self.touch();
162
        let Elder::Unverified { strikes } = &mut self.elder else {
163
            return problem;
164
        };
165
        *strikes += 1;
166
        if *strikes < PATIENCE {
167
            return problem;
168
        }
169
        self.end(Cause::Banned)
170
    }
171
172
    /// Failed verification counts toward a ban even on GET, the way repeated bad credentials lock an account.
173
    fn verify_member(&mut self, proxy_authorization: Option<&str>) -> Result<(), Problem> {
174
        if !matches!(self.elder, Elder::Unverified { .. }) {
175
            return Ok(());
176
        }
177
        let Some(credentials) = proxy_authorization else {
178
            return Err(verification("The forum requires verification."));
179
        };
180
        let Some(claimed) =
181
            parse_credentials(credentials, "ruler").and_then(|v| v.parse::<f64>().ok())
182
        else {
183
            return Err(verification("The forum requires verification, in inches."));
184
        };
185
        let actual = self.current_length();
186
        if claimed >= actual + EXAGGERATION {
187
            self.shame += 2;
188
            return Err(self.elder_strike(exaggeration()));
189
        }
190
        if !honest(claimed, actual) {
191
            self.shame += 1;
192
            return Err(self.elder_strike(verification("Verification failed. The forum can tell.")));
193
        }
194
        self.elder = Elder::Verified;
195
        self.touch();
196
        Ok(())
197
    }
198
199
    pub fn elder(
200
        &mut self,
201
        proxy_authorization: Option<&str>,
202
        if_modified_since: Option<&str>,
203
    ) -> Result<Outcome, Problem> {
204
        if self.floor != Floor::Forum {
205
            return Err(Problem::new(
206
                StatusCode::NOT_FOUND,
207
                "elder-absent",
208
                "The Elder only posts on the forum.",
209
            ));
210
        }
211
        self.verify_member(proxy_authorization)?;
212
213
        let last_modified =
214
            httpdate::parse_http_date(ELDER_LAST_MODIFIED).expect("valid constant date");
215
        let current = if_modified_since
216
            .and_then(|d| httpdate::parse_http_date(d).ok())
217
            .is_some_and(|since| since >= last_modified);
218
        if current {
219
            if self.elder != Elder::Beaten {
220
                self.elder = Elder::Beaten;
221
                self.touch();
222
            }
223
            return Ok(Outcome::empty(StatusCode::NOT_MODIFIED)
224
                .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED));
225
        }
226
        let spreadsheet = Outcome::raw(StatusCode::OK, "text/csv; header=present", SPREADSHEET)
227
            .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED);
228
        if self.filmed_at.is_some() {
229
            return Ok(spreadsheet.header(X_FORUM_NOTE, "We have seen the footage."));
230
        }
231
        Ok(spreadsheet)
232
    }
233
}