atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
b85a0d4536c3b294547a682ac046d8a38beb47a3
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/game/forum.rs

782069860236atagen2026-10-10 12:44 UTC1
use axum::http::{StatusCode, header};
782069860236atagen2026-10-10 12:44 UTC2
use serde_json::{Value, json};
782069860236atagen2026-10-10 12:44 UTC3
782069860236atagen2026-10-10 12:44 UTC4
use super::{
782069860236atagen2026-10-10 12:44 UTC5
    Cause, Elder, Floor, Outcome, Run, Story,
b85a0d4536c3atagen2026-10-11 08:50 UTC6
    balance::{EXAGGERATION, PATIENCE, SUSPICION_LIMIT, ULI_ODDS},
782069860236atagen2026-10-10 12:44 UTC7
    body::exaggeration,
782069860236atagen2026-10-10 12:44 UTC8
    honest, parse_credentials,
782069860236atagen2026-10-10 12:44 UTC9
};
782069860236atagen2026-10-10 12:44 UTC10
use crate::{headers::X_FORUM_NOTE, problem::Problem};
782069860236atagen2026-10-10 12:44 UTC11
782069860236atagen2026-10-10 12:44 UTC12
pub const FORUM_THREAD: &str = "/forum/threads/2009";
782069860236atagen2026-10-10 12:44 UTC13
pub const ELDER_LAST_MODIFIED: &str = "Sun, 14 Jun 2009 03:12:00 GMT";
782069860236atagen2026-10-10 12:44 UTC14
782069860236atagen2026-10-10 12:44 UTC15
const SPREADSHEET: &str = "\
782069860236atagen2026-10-10 12:44 UTC16
date,length,girth,technique,notes,honest?
782069860236atagen2026-10-10 12:44 UTC17
2007-01-03,7.50,5.50,wet,baseline,no
782069860236atagen2026-10-10 12:44 UTC18
2007-06-11,7.50,5.50,wet,feeling fuller,no
782069860236atagen2026-10-10 12:44 UTC19
2008-01-02,7.50,5.50,dry,new routine,no
782069860236atagen2026-10-10 12:44 UTC20
2008-09-19,7.50,5.50,uli,PUT is idempotent. do not use PUT.,no
782069860236atagen2026-10-10 12:44 UTC21
2009-06-14,7.50,5.50,wet,progress is slow but real,no
782069860236atagen2026-10-10 12:44 UTC22
";
782069860236atagen2026-10-10 12:44 UTC23
782069860236atagen2026-10-10 12:44 UTC24
pub fn forum_thread() -> Value {
782069860236atagen2026-10-10 12:44 UTC25
    json!({
782069860236atagen2026-10-10 12:44 UTC26
        "title": "The definitive jelqing FAQ (updated 2009)",
782069860236atagen2026-10-10 12:44 UTC27
        "posts": [
782069860236atagen2026-10-10 12:44 UTC28
            { "author": "TheElder", "body": "Warm up. Always warm up. A cold jelq is a 412 waiting to happen." },
782069860236atagen2026-10-10 12:44 UTC29
            { "author": "TheElder", "body": "Keep EQ between 50 and 70. Above 70 you are gambling. Above 90 you are a cautionary tale." },
0fdc47364c8catagen2026-10-11 04:45 UTC30
            { "author": "newbie_2009", "body": "everyone keeps saying uli. who is uli" },
0fdc47364c8catagen2026-10-11 04:45 UTC31
            { "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." },
0fdc47364c8catagen2026-10-11 04:45 UTC32
            { "author": "TheElder", "body": "We do not say his name in vain. We say it in girth." },
782069860236atagen2026-10-10 12:44 UTC33
            { "author": "newbie_2009", "body": "I have been sending PUT for three weeks and nothing has changed." },
782069860236atagen2026-10-10 12:44 UTC34
            { "author": "TheElder", "body": "PUT is idempotent. Read the spec." },
782069860236atagen2026-10-10 12:44 UTC35
            { "author": "TheElder", "body": "Members verify with a Ruler. Proxy-Authorization, inches, no rounding up. We can tell." },
782069860236atagen2026-10-10 12:44 UTC36
            { "author": "throwaway_77", "body": "my doctor said i was normal. i didnt believe him. i still dont know what he meant by bearer" },
782069860236atagen2026-10-10 12:44 UTC37
            { "author": "TheElder", "body": "My numbers are in my spreadsheet. They have not changed since 2009. Nothing about me has changed since 2009." },
782069860236atagen2026-10-10 12:44 UTC38
        ],
782069860236atagen2026-10-10 12:44 UTC39
    })
782069860236atagen2026-10-10 12:44 UTC40
}
782069860236atagen2026-10-10 12:44 UTC41
782069860236atagen2026-10-10 12:44 UTC42
fn verification(detail: &str) -> Problem {
782069860236atagen2026-10-10 12:44 UTC43
    Problem::new(
782069860236atagen2026-10-10 12:44 UTC44
        StatusCode::PROXY_AUTHENTICATION_REQUIRED,
782069860236atagen2026-10-10 12:44 UTC45
        "verification-required",
782069860236atagen2026-10-10 12:44 UTC46
        detail,
782069860236atagen2026-10-10 12:44 UTC47
    )
782069860236atagen2026-10-10 12:44 UTC48
    .header(header::PROXY_AUTHENTICATE, "Ruler realm=\"forum\"")
782069860236atagen2026-10-10 12:44 UTC49
}
782069860236atagen2026-10-10 12:44 UTC50
782069860236atagen2026-10-10 12:44 UTC51
impl Run {
782069860236atagen2026-10-10 12:44 UTC52
    pub(super) fn branch_offer(&self) -> Outcome {
782069860236atagen2026-10-10 12:44 UTC53
        let forum = self.path("/branches/forum");
782069860236atagen2026-10-10 12:44 UTC54
        let close = self.path("/branches/close-tab");
782069860236atagen2026-10-10 12:44 UTC55
        Outcome::json(
782069860236atagen2026-10-10 12:44 UTC56
            StatusCode::MULTIPLE_CHOICES,
782069860236atagen2026-10-10 12:44 UTC57
            json!({
782069860236atagen2026-10-10 12:44 UTC58
                "detail": "A browser tab is open. It has been open for some time.",
782069860236atagen2026-10-10 12:44 UTC59
                "choices": [
782069860236atagen2026-10-10 12:44 UTC60
                    { "href": forum, "method": "POST", "title": "Read the forum" },
782069860236atagen2026-10-10 12:44 UTC61
                    { "href": close, "method": "POST", "title": "Close the tab" },
782069860236atagen2026-10-10 12:44 UTC62
                ],
782069860236atagen2026-10-10 12:44 UTC63
            }),
782069860236atagen2026-10-10 12:44 UTC64
        )
782069860236atagen2026-10-10 12:44 UTC65
        .header(
782069860236atagen2026-10-10 12:44 UTC66
            header::LINK,
782069860236atagen2026-10-10 12:44 UTC67
            format!(
d46f9d0b953aatagen2026-10-10 14:41 UTC68
                "<{forum}>; rel=\"alternate\"; title=\"Read the forum\"; allow=\"POST\", <{close}>; rel=\"alternate\"; title=\"Close the tab\"; allow=\"POST\""
782069860236atagen2026-10-10 12:44 UTC69
            ),
782069860236atagen2026-10-10 12:44 UTC70
        )
782069860236atagen2026-10-10 12:44 UTC71
    }
782069860236atagen2026-10-10 12:44 UTC72
782069860236atagen2026-10-10 12:44 UTC73
    pub fn choose(&mut self, choice: &str) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC74
        if self.story != Story::Pending {
782069860236atagen2026-10-10 12:44 UTC75
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC76
                StatusCode::CONFLICT,
782069860236atagen2026-10-10 12:44 UTC77
                "no-decision-pending",
782069860236atagen2026-10-10 12:44 UTC78
                "There is no decision pending.",
782069860236atagen2026-10-10 12:44 UTC79
            ));
782069860236atagen2026-10-10 12:44 UTC80
        }
782069860236atagen2026-10-10 12:44 UTC81
        let (story, location, detail) = match choice {
782069860236atagen2026-10-10 12:44 UTC82
            "forum" => {
782069860236atagen2026-10-10 12:44 UTC83
                self.shame += 3;
b85a0d4536c3atagen2026-10-11 08:50 UTC84
                let detail =
b85a0d4536c3atagen2026-10-11 08:50 UTC85
                    "The thread is from 2009. A technique called uli is discussed at length.";
0fdc47364c8catagen2026-10-11 04:45 UTC86
                (Story::ReadForum, self.path("/techniques"), detail)
782069860236atagen2026-10-10 12:44 UTC87
            }
782069860236atagen2026-10-10 12:44 UTC88
            "close-tab" => {
782069860236atagen2026-10-10 12:44 UTC89
                self.suspicion = 0;
782069860236atagen2026-10-10 12:44 UTC90
                (
782069860236atagen2026-10-10 12:44 UTC91
                    Story::ClosedTab,
782069860236atagen2026-10-10 12:44 UTC92
                    self.path(""),
782069860236atagen2026-10-10 12:44 UTC93
                    "The tab is closed. Nobody saw it.",
782069860236atagen2026-10-10 12:44 UTC94
                )
782069860236atagen2026-10-10 12:44 UTC95
            }
782069860236atagen2026-10-10 12:44 UTC96
            _ => {
782069860236atagen2026-10-10 12:44 UTC97
                return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC98
                    StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC99
                    "no-such-choice",
782069860236atagen2026-10-10 12:44 UTC100
                    "That was not one of the choices.",
782069860236atagen2026-10-10 12:44 UTC101
                ));
782069860236atagen2026-10-10 12:44 UTC102
            }
782069860236atagen2026-10-10 12:44 UTC103
        };
782069860236atagen2026-10-10 12:44 UTC104
        self.story = story;
782069860236atagen2026-10-10 12:44 UTC105
        self.touch();
782069860236atagen2026-10-10 12:44 UTC106
        Ok(
782069860236atagen2026-10-10 12:44 UTC107
            Outcome::json(StatusCode::SEE_OTHER, json!({ "detail": detail }))
782069860236atagen2026-10-10 12:44 UTC108
                .header(header::LOCATION, location),
782069860236atagen2026-10-10 12:44 UTC109
        )
782069860236atagen2026-10-10 12:44 UTC110
    }
782069860236atagen2026-10-10 12:44 UTC111
b85a0d4536c3atagen2026-10-11 08:50 UTC112
    /// Opening the thread again costs the turn, and each read may finally surface the reply.
b85a0d4536c3atagen2026-10-11 08:50 UTC113
    pub fn read_thread(&mut self) -> Result<Outcome, Problem> {
b85a0d4536c3atagen2026-10-11 08:50 UTC114
        if self.story == Story::Pending {
b85a0d4536c3atagen2026-10-11 08:50 UTC115
            return Ok(self.branch_offer());
b85a0d4536c3atagen2026-10-11 08:50 UTC116
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC117
        self.touch();
b85a0d4536c3atagen2026-10-11 08:50 UTC118
        self.turn += 1;
b85a0d4536c3atagen2026-10-11 08:50 UTC119
        self.here.turns += 1;
b85a0d4536c3atagen2026-10-11 08:50 UTC120
        self.realise();
b85a0d4536c3atagen2026-10-11 08:50 UTC121
        if self.chance(self.rupture_odds()) {
b85a0d4536c3atagen2026-10-11 08:50 UTC122
            return Err(self.rupture());
b85a0d4536c3atagen2026-10-11 08:50 UTC123
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC124
        if self.chance(ULI_ODDS) {
b85a0d4536c3atagen2026-10-11 08:50 UTC125
            self.uli_taught = true;
b85a0d4536c3atagen2026-10-11 08:50 UTC126
            return Ok(Outcome::json(
b85a0d4536c3atagen2026-10-11 08:50 UTC127
                StatusCode::OK,
b85a0d4536c3atagen2026-10-11 08:50 UTC128
                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." }),
b85a0d4536c3atagen2026-10-11 08:50 UTC129
            )
b85a0d4536c3atagen2026-10-11 08:50 UTC130
            .header(header::LOCATION, self.path("/techniques")));
b85a0d4536c3atagen2026-10-11 08:50 UTC131
        }
b85a0d4536c3atagen2026-10-11 08:50 UTC132
        let was_below = self.suspicion < SUSPICION_LIMIT;
b85a0d4536c3atagen2026-10-11 08:50 UTC133
        self.settle(was_below);
b85a0d4536c3atagen2026-10-11 08:50 UTC134
        Ok(Outcome::empty(StatusCode::NOT_MODIFIED).header(
b85a0d4536c3atagen2026-10-11 08:50 UTC135
            header::WARNING,
b85a0d4536c3atagen2026-10-11 08:50 UTC136
            r#"199 - "Nothing new has been posted since 2009.""#,
b85a0d4536c3atagen2026-10-11 08:50 UTC137
        ))
b85a0d4536c3atagen2026-10-11 08:50 UTC138
    }
b85a0d4536c3atagen2026-10-11 08:50 UTC139
782069860236atagen2026-10-10 12:44 UTC140
    fn elder_strike(&mut self, problem: Problem) -> Problem {
782069860236atagen2026-10-10 12:44 UTC141
        self.touch();
782069860236atagen2026-10-10 12:44 UTC142
        let Elder::Unverified { strikes } = &mut self.elder else {
782069860236atagen2026-10-10 12:44 UTC143
            return problem;
782069860236atagen2026-10-10 12:44 UTC144
        };
782069860236atagen2026-10-10 12:44 UTC145
        *strikes += 1;
782069860236atagen2026-10-10 12:44 UTC146
        if *strikes < PATIENCE {
782069860236atagen2026-10-10 12:44 UTC147
            return problem;
782069860236atagen2026-10-10 12:44 UTC148
        }
782069860236atagen2026-10-10 12:44 UTC149
        self.end(Cause::Banned)
782069860236atagen2026-10-10 12:44 UTC150
    }
782069860236atagen2026-10-10 12:44 UTC151
782069860236atagen2026-10-10 12:44 UTC152
    /// Failed verification counts toward a ban even on GET, the way repeated bad credentials lock an account.
782069860236atagen2026-10-10 12:44 UTC153
    fn verify_member(&mut self, proxy_authorization: Option<&str>) -> Result<(), Problem> {
782069860236atagen2026-10-10 12:44 UTC154
        if !matches!(self.elder, Elder::Unverified { .. }) {
782069860236atagen2026-10-10 12:44 UTC155
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC156
        }
782069860236atagen2026-10-10 12:44 UTC157
        let Some(credentials) = proxy_authorization else {
782069860236atagen2026-10-10 12:44 UTC158
            return Err(verification("The forum requires verification."));
782069860236atagen2026-10-10 12:44 UTC159
        };
782069860236atagen2026-10-10 12:44 UTC160
        let Some(claimed) =
782069860236atagen2026-10-10 12:44 UTC161
            parse_credentials(credentials, "ruler").and_then(|v| v.parse::<f64>().ok())
782069860236atagen2026-10-10 12:44 UTC162
        else {
782069860236atagen2026-10-10 12:44 UTC163
            return Err(verification("The forum requires verification, in inches."));
782069860236atagen2026-10-10 12:44 UTC164
        };
782069860236atagen2026-10-10 12:44 UTC165
        let actual = self.current_length();
782069860236atagen2026-10-10 12:44 UTC166
        if claimed >= actual + EXAGGERATION {
782069860236atagen2026-10-10 12:44 UTC167
            self.shame += 2;
782069860236atagen2026-10-10 12:44 UTC168
            return Err(self.elder_strike(exaggeration()));
782069860236atagen2026-10-10 12:44 UTC169
        }
782069860236atagen2026-10-10 12:44 UTC170
        if !honest(claimed, actual) {
782069860236atagen2026-10-10 12:44 UTC171
            self.shame += 1;
782069860236atagen2026-10-10 12:44 UTC172
            return Err(self.elder_strike(verification("Verification failed. The forum can tell.")));
782069860236atagen2026-10-10 12:44 UTC173
        }
782069860236atagen2026-10-10 12:44 UTC174
        self.elder = Elder::Verified;
782069860236atagen2026-10-10 12:44 UTC175
        self.touch();
782069860236atagen2026-10-10 12:44 UTC176
        Ok(())
782069860236atagen2026-10-10 12:44 UTC177
    }
782069860236atagen2026-10-10 12:44 UTC178
782069860236atagen2026-10-10 12:44 UTC179
    pub fn elder(
782069860236atagen2026-10-10 12:44 UTC180
        &mut self,
782069860236atagen2026-10-10 12:44 UTC181
        proxy_authorization: Option<&str>,
782069860236atagen2026-10-10 12:44 UTC182
        if_modified_since: Option<&str>,
782069860236atagen2026-10-10 12:44 UTC183
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC184
        if self.floor != Floor::Forum {
782069860236atagen2026-10-10 12:44 UTC185
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC186
                StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC187
                "elder-absent",
782069860236atagen2026-10-10 12:44 UTC188
                "The Elder only posts on the forum.",
782069860236atagen2026-10-10 12:44 UTC189
            ));
782069860236atagen2026-10-10 12:44 UTC190
        }
782069860236atagen2026-10-10 12:44 UTC191
        self.verify_member(proxy_authorization)?;
782069860236atagen2026-10-10 12:44 UTC192
782069860236atagen2026-10-10 12:44 UTC193
        let last_modified =
782069860236atagen2026-10-10 12:44 UTC194
            httpdate::parse_http_date(ELDER_LAST_MODIFIED).expect("valid constant date");
782069860236atagen2026-10-10 12:44 UTC195
        let current = if_modified_since
782069860236atagen2026-10-10 12:44 UTC196
            .and_then(|d| httpdate::parse_http_date(d).ok())
782069860236atagen2026-10-10 12:44 UTC197
            .is_some_and(|since| since >= last_modified);
782069860236atagen2026-10-10 12:44 UTC198
        if current {
782069860236atagen2026-10-10 12:44 UTC199
            if self.elder != Elder::Beaten {
782069860236atagen2026-10-10 12:44 UTC200
                self.elder = Elder::Beaten;
782069860236atagen2026-10-10 12:44 UTC201
                self.touch();
782069860236atagen2026-10-10 12:44 UTC202
            }
782069860236atagen2026-10-10 12:44 UTC203
            return Ok(Outcome::empty(StatusCode::NOT_MODIFIED)
782069860236atagen2026-10-10 12:44 UTC204
                .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED));
782069860236atagen2026-10-10 12:44 UTC205
        }
782069860236atagen2026-10-10 12:44 UTC206
        let spreadsheet = Outcome::raw(StatusCode::OK, "text/csv; header=present", SPREADSHEET)
782069860236atagen2026-10-10 12:44 UTC207
            .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED);
782069860236atagen2026-10-10 12:44 UTC208
        if self.filmed_at.is_some() {
782069860236atagen2026-10-10 12:44 UTC209
            return Ok(spreadsheet.header(X_FORUM_NOTE, "We have seen the footage."));
782069860236atagen2026-10-10 12:44 UTC210
        }
782069860236atagen2026-10-10 12:44 UTC211
        Ok(spreadsheet)
782069860236atagen2026-10-10 12:44 UTC212
    }
782069860236atagen2026-10-10 12:44 UTC213
}