atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
d604371299fbbc6fbf489e054ac9390a97b7ac55
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/forum.rsRust184 lines6.8 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},
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": "I have been sending PUT for three weeks and nothing has changed." },
31
            { "author": "TheElder", "body": "PUT is idempotent. Read the spec." },
32
            { "author": "TheElder", "body": "Members verify with a Ruler. Proxy-Authorization, inches, no rounding up. We can tell." },
33
            { "author": "throwaway_77", "body": "my doctor said i was normal. i didnt believe him. i still dont know what he meant by bearer" },
34
            { "author": "TheElder", "body": "My numbers are in my spreadsheet. They have not changed since 2009. Nothing about me has changed since 2009." },
35
        ],
36
    })
37
}
38
39
fn verification(detail: &str) -> Problem {
40
    Problem::new(
41
        StatusCode::PROXY_AUTHENTICATION_REQUIRED,
42
        "verification-required",
43
        detail,
44
    )
45
    .header(header::PROXY_AUTHENTICATE, "Ruler realm=\"forum\"")
46
}
47
48
impl Run {
49
    pub(super) fn branch_offer(&self) -> Outcome {
50
        let forum = self.path("/branches/forum");
51
        let close = self.path("/branches/close-tab");
52
        Outcome::json(
53
            StatusCode::MULTIPLE_CHOICES,
54
            json!({
55
                "detail": "A browser tab is open. It has been open for some time.",
56
                "choices": [
57
                    { "href": forum, "method": "POST", "title": "Read the forum" },
58
                    { "href": close, "method": "POST", "title": "Close the tab" },
59
                ],
60
            }),
61
        )
62
        .header(
63
            header::LINK,
64
            format!(
65
                "<{forum}>; rel=\"alternate\"; title=\"Read the forum\", <{close}>; rel=\"alternate\"; title=\"Close the tab\""
66
            ),
67
        )
68
    }
69
70
    pub fn choose(&mut self, choice: &str) -> Result<Outcome, Problem> {
71
        if self.story != Story::Pending {
72
            return Err(Problem::new(
73
                StatusCode::CONFLICT,
74
                "no-decision-pending",
75
                "There is no decision pending.",
76
            ));
77
        }
78
        let (story, location, detail) = match choice {
79
            "forum" => {
80
                self.shame += 3;
81
                (
82
                    Story::ReadForum,
83
                    self.path("/techniques"),
84
                    "The thread is from 2009. A technique called uli is discussed at length.",
85
                )
86
            }
87
            "close-tab" => {
88
                self.suspicion = 0;
89
                (
90
                    Story::ClosedTab,
91
                    self.path(""),
92
                    "The tab is closed. Nobody saw it.",
93
                )
94
            }
95
            _ => {
96
                return Err(Problem::new(
97
                    StatusCode::NOT_FOUND,
98
                    "no-such-choice",
99
                    "That was not one of the choices.",
100
                ));
101
            }
102
        };
103
        self.story = story;
104
        self.touch();
105
        Ok(
106
            Outcome::json(StatusCode::SEE_OTHER, json!({ "detail": detail }))
107
                .header(header::LOCATION, location),
108
        )
109
    }
110
111
    fn elder_strike(&mut self, problem: Problem) -> Problem {
112
        self.touch();
113
        let Elder::Unverified { strikes } = &mut self.elder else {
114
            return problem;
115
        };
116
        *strikes += 1;
117
        if *strikes < PATIENCE {
118
            return problem;
119
        }
120
        self.end(Cause::Banned)
121
    }
122
123
    /// Failed verification counts toward a ban even on GET, the way repeated bad credentials lock an account.
124
    fn verify_member(&mut self, proxy_authorization: Option<&str>) -> Result<(), Problem> {
125
        if !matches!(self.elder, Elder::Unverified { .. }) {
126
            return Ok(());
127
        }
128
        let Some(credentials) = proxy_authorization else {
129
            return Err(verification("The forum requires verification."));
130
        };
131
        let Some(claimed) =
132
            parse_credentials(credentials, "ruler").and_then(|v| v.parse::<f64>().ok())
133
        else {
134
            return Err(verification("The forum requires verification, in inches."));
135
        };
136
        let actual = self.current_length();
137
        if claimed >= actual + EXAGGERATION {
138
            self.shame += 2;
139
            return Err(self.elder_strike(exaggeration()));
140
        }
141
        if !honest(claimed, actual) {
142
            self.shame += 1;
143
            return Err(self.elder_strike(verification("Verification failed. The forum can tell.")));
144
        }
145
        self.elder = Elder::Verified;
146
        self.touch();
147
        Ok(())
148
    }
149
150
    pub fn elder(
151
        &mut self,
152
        proxy_authorization: Option<&str>,
153
        if_modified_since: Option<&str>,
154
    ) -> Result<Outcome, Problem> {
155
        if self.floor != Floor::Forum {
156
            return Err(Problem::new(
157
                StatusCode::NOT_FOUND,
158
                "elder-absent",
159
                "The Elder only posts on the forum.",
160
            ));
161
        }
162
        self.verify_member(proxy_authorization)?;
163
164
        let last_modified =
165
            httpdate::parse_http_date(ELDER_LAST_MODIFIED).expect("valid constant date");
166
        let current = if_modified_since
167
            .and_then(|d| httpdate::parse_http_date(d).ok())
168
            .is_some_and(|since| since >= last_modified);
169
        if current {
170
            if self.elder != Elder::Beaten {
171
                self.elder = Elder::Beaten;
172
                self.touch();
173
            }
174
            return Ok(Outcome::empty(StatusCode::NOT_MODIFIED)
175
                .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED));
176
        }
177
        let spreadsheet = Outcome::raw(StatusCode::OK, "text/csv; header=present", SPREADSHEET)
178
            .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED);
179
        if self.filmed_at.is_some() {
180
            return Ok(spreadsheet.header(X_FORUM_NOTE, "We have seen the footage."));
181
        }
182
        Ok(spreadsheet)
183
    }
184
}