atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/forum.rs

8b8e83329a1datagen2026-10-12 12:01 UTC1
use std::{sync::LazyLock, time::SystemTime};
a6caa2f99786atagen2026-10-12 10:38 UTC2
782069860236atagen2026-10-10 12:44 UTC3
use axum::http::{StatusCode, header};
782069860236atagen2026-10-10 12:44 UTC4
use serde_json::{Value, json};
782069860236atagen2026-10-10 12:44 UTC5
782069860236atagen2026-10-10 12:44 UTC6
use super::{
56d34031c321atagen2026-10-14 09:23 UTC7
    Cause, Floor, Item, Outcome, Run,
56d34031c321atagen2026-10-14 09:23 UTC8
    balance::{
56d34031c321atagen2026-10-14 09:23 UTC9
        EXAGGERATION, MEDITATION_ODDS, PATIENCE, PRINTABLE_RULER, RULER_SHAME, THREAD_READS,
56d34031c321atagen2026-10-14 09:23 UTC10
        ULI_ODDS,
56d34031c321atagen2026-10-14 09:23 UTC11
    },
8b8e83329a1datagen2026-10-12 12:01 UTC12
    credentials::{Scheme, parse_credentials},
8b8e83329a1datagen2026-10-12 12:01 UTC13
    measure::{exaggeration, honest},
782069860236atagen2026-10-10 12:44 UTC14
};
782069860236atagen2026-10-10 12:44 UTC15
use crate::{headers::X_FORUM_NOTE, problem::Problem};
782069860236atagen2026-10-10 12:44 UTC16
782069860236atagen2026-10-10 12:44 UTC17
pub const FORUM_THREAD: &str = "/forum/threads/2009";
782069860236atagen2026-10-10 12:44 UTC18
pub const ELDER_LAST_MODIFIED: &str = "Sun, 14 Jun 2009 03:12:00 GMT";
8b8e83329a1datagen2026-10-12 12:01 UTC19
static ELDER_POSTED: LazyLock<SystemTime> = LazyLock::new(|| {
8b8e83329a1datagen2026-10-12 12:01 UTC20
    httpdate::parse_http_date(ELDER_LAST_MODIFIED).expect("the Elder's date is a valid HTTP-date")
8b8e83329a1datagen2026-10-12 12:01 UTC21
});
782069860236atagen2026-10-10 12:44 UTC22
782069860236atagen2026-10-10 12:44 UTC23
const SPREADSHEET: &str = "\
782069860236atagen2026-10-10 12:44 UTC24
date,length,girth,technique,notes,honest?
782069860236atagen2026-10-10 12:44 UTC25
2007-01-03,7.50,5.50,wet,baseline,no
782069860236atagen2026-10-10 12:44 UTC26
2007-06-11,7.50,5.50,wet,feeling fuller,no
782069860236atagen2026-10-10 12:44 UTC27
2008-01-02,7.50,5.50,dry,new routine,no
782069860236atagen2026-10-10 12:44 UTC28
2008-09-19,7.50,5.50,uli,PUT is idempotent. do not use PUT.,no
782069860236atagen2026-10-10 12:44 UTC29
2009-06-14,7.50,5.50,wet,progress is slow but real,no
782069860236atagen2026-10-10 12:44 UTC30
";
782069860236atagen2026-10-10 12:44 UTC31
a11497af08aaatagen2026-10-13 12:04 UTC32
fn thread(detail: Option<&str>) -> Outcome {
a11497af08aaatagen2026-10-13 12:04 UTC33
    let mut body = forum_thread();
a11497af08aaatagen2026-10-13 12:04 UTC34
    if let Some(detail) = detail {
a11497af08aaatagen2026-10-13 12:04 UTC35
        body["detail"] = json!(detail);
a11497af08aaatagen2026-10-13 12:04 UTC36
    }
a11497af08aaatagen2026-10-13 12:04 UTC37
    Outcome::json(StatusCode::OK, body).header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED)
a11497af08aaatagen2026-10-13 12:04 UTC38
}
a11497af08aaatagen2026-10-13 12:04 UTC39
782069860236atagen2026-10-10 12:44 UTC40
pub fn forum_thread() -> Value {
782069860236atagen2026-10-10 12:44 UTC41
    json!({
782069860236atagen2026-10-10 12:44 UTC42
        "title": "The definitive jelqing FAQ (updated 2009)",
782069860236atagen2026-10-10 12:44 UTC43
        "posts": [
782069860236atagen2026-10-10 12:44 UTC44
            { "author": "TheElder", "body": "Warm up. Always warm up. A cold jelq is a 412 waiting to happen." },
e028a4184d00atagen2026-10-14 10:49 UTC45
            { "author": "TheElder", "body": "Keep it Ideal. Swollen is gambling. Straining is a cautionary tale." },
0fdc47364c8catagen2026-10-11 04:45 UTC46
            { "author": "newbie_2009", "body": "everyone keeps saying uli. who is uli" },
0fdc47364c8catagen2026-10-11 04:45 UTC47
            { "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 UTC48
            { "author": "TheElder", "body": "We do not say his name in vain. We say it in girth." },
782069860236atagen2026-10-10 12:44 UTC49
            { "author": "newbie_2009", "body": "I have been sending PUT for three weeks and nothing has changed." },
782069860236atagen2026-10-10 12:44 UTC50
            { "author": "TheElder", "body": "PUT is idempotent. Read the spec." },
782069860236atagen2026-10-10 12:44 UTC51
            { "author": "TheElder", "body": "Members verify with a Ruler. Proxy-Authorization, inches, no rounding up. We can tell." },
9adb509e6685atagen2026-10-14 08:54 UTC52
            { "author": "newbie_2009", "body": "cant i just take off what i tell people. everyone adds an inch and a half" },
9adb509e6685atagen2026-10-14 08:54 UTC53
            { "author": "TheElder", "body": "Everyone adds a different amount. That is why there is a Ruler." },
782069860236atagen2026-10-10 12:44 UTC54
            { "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 UTC55
            { "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 UTC56
        ],
782069860236atagen2026-10-10 12:44 UTC57
    })
782069860236atagen2026-10-10 12:44 UTC58
}
782069860236atagen2026-10-10 12:44 UTC59
782069860236atagen2026-10-10 12:44 UTC60
fn verification(detail: &str) -> Problem {
782069860236atagen2026-10-10 12:44 UTC61
    Problem::new(
782069860236atagen2026-10-10 12:44 UTC62
        StatusCode::PROXY_AUTHENTICATION_REQUIRED,
782069860236atagen2026-10-10 12:44 UTC63
        "verification-required",
782069860236atagen2026-10-10 12:44 UTC64
        detail,
782069860236atagen2026-10-10 12:44 UTC65
    )
782069860236atagen2026-10-10 12:44 UTC66
    .header(header::PROXY_AUTHENTICATE, "Ruler realm=\"forum\"")
782069860236atagen2026-10-10 12:44 UTC67
}
782069860236atagen2026-10-10 12:44 UTC68
8b8e83329a1datagen2026-10-12 12:01 UTC69
/// The browser tab that opens on turn 10.
8b8e83329a1datagen2026-10-12 12:01 UTC70
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8b8e83329a1datagen2026-10-12 12:01 UTC71
pub enum Story {
8b8e83329a1datagen2026-10-12 12:01 UTC72
    Unopened,
8b8e83329a1datagen2026-10-12 12:01 UTC73
    Pending,
8b8e83329a1datagen2026-10-12 12:01 UTC74
    ReadForum,
8b8e83329a1datagen2026-10-12 12:01 UTC75
    ClosedTab,
8b8e83329a1datagen2026-10-12 12:01 UTC76
}
8b8e83329a1datagen2026-10-12 12:01 UTC77
8b8e83329a1datagen2026-10-12 12:01 UTC78
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8b8e83329a1datagen2026-10-12 12:01 UTC79
pub enum Branch {
8b8e83329a1datagen2026-10-12 12:01 UTC80
    Forum,
8b8e83329a1datagen2026-10-12 12:01 UTC81
    CloseTab,
8b8e83329a1datagen2026-10-12 12:01 UTC82
}
8b8e83329a1datagen2026-10-12 12:01 UTC83
8b8e83329a1datagen2026-10-12 12:01 UTC84
impl Branch {
8b8e83329a1datagen2026-10-12 12:01 UTC85
    pub fn slug(self) -> &'static str {
8b8e83329a1datagen2026-10-12 12:01 UTC86
        match self {
8b8e83329a1datagen2026-10-12 12:01 UTC87
            Self::Forum => "forum",
8b8e83329a1datagen2026-10-12 12:01 UTC88
            Self::CloseTab => "close-tab",
8b8e83329a1datagen2026-10-12 12:01 UTC89
        }
8b8e83329a1datagen2026-10-12 12:01 UTC90
    }
8b8e83329a1datagen2026-10-12 12:01 UTC91
}
8b8e83329a1datagen2026-10-12 12:01 UTC92
8b8e83329a1datagen2026-10-12 12:01 UTC93
impl std::str::FromStr for Branch {
8b8e83329a1datagen2026-10-12 12:01 UTC94
    type Err = ();
8b8e83329a1datagen2026-10-12 12:01 UTC95
8b8e83329a1datagen2026-10-12 12:01 UTC96
    fn from_str(slug: &str) -> Result<Self, ()> {
8b8e83329a1datagen2026-10-12 12:01 UTC97
        [Self::Forum, Self::CloseTab]
8b8e83329a1datagen2026-10-12 12:01 UTC98
            .into_iter()
8b8e83329a1datagen2026-10-12 12:01 UTC99
            .find(|branch| branch.slug() == slug)
8b8e83329a1datagen2026-10-12 12:01 UTC100
            .ok_or(())
8b8e83329a1datagen2026-10-12 12:01 UTC101
    }
8b8e83329a1datagen2026-10-12 12:01 UTC102
}
8b8e83329a1datagen2026-10-12 12:01 UTC103
8b8e83329a1datagen2026-10-12 12:01 UTC104
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8b8e83329a1datagen2026-10-12 12:01 UTC105
pub enum Elder {
8b8e83329a1datagen2026-10-12 12:01 UTC106
    Unverified { strikes: u8 },
8b8e83329a1datagen2026-10-12 12:01 UTC107
    Verified,
8b8e83329a1datagen2026-10-12 12:01 UTC108
    Beaten,
8b8e83329a1datagen2026-10-12 12:01 UTC109
}
8b8e83329a1datagen2026-10-12 12:01 UTC110
8b8e83329a1datagen2026-10-12 12:01 UTC111
/// The run's life online, from the tab on turn 10 to the footage the neighbour took.
8b8e83329a1datagen2026-10-12 12:01 UTC112
pub struct Online {
8b8e83329a1datagen2026-10-12 12:01 UTC113
    pub(crate) story: Story,
880b29a0c11eatagen2026-10-13 09:27 UTC114
    pub(crate) meditation_taught: bool,
8b8e83329a1datagen2026-10-12 12:01 UTC115
    pub(crate) uli_taught: bool,
8b8e83329a1datagen2026-10-12 12:01 UTC116
    pub(crate) elder: Elder,
8b8e83329a1datagen2026-10-12 12:01 UTC117
    pub(crate) filmed_at: Option<SystemTime>,
8b8e83329a1datagen2026-10-12 12:01 UTC118
}
8b8e83329a1datagen2026-10-12 12:01 UTC119
8b8e83329a1datagen2026-10-12 12:01 UTC120
impl Default for Online {
8b8e83329a1datagen2026-10-12 12:01 UTC121
    fn default() -> Self {
8b8e83329a1datagen2026-10-12 12:01 UTC122
        Self {
8b8e83329a1datagen2026-10-12 12:01 UTC123
            story: Story::Unopened,
880b29a0c11eatagen2026-10-13 09:27 UTC124
            meditation_taught: false,
8b8e83329a1datagen2026-10-12 12:01 UTC125
            uli_taught: false,
8b8e83329a1datagen2026-10-12 12:01 UTC126
            elder: Elder::Unverified { strikes: 0 },
8b8e83329a1datagen2026-10-12 12:01 UTC127
            filmed_at: None,
8b8e83329a1datagen2026-10-12 12:01 UTC128
        }
8b8e83329a1datagen2026-10-12 12:01 UTC129
    }
8b8e83329a1datagen2026-10-12 12:01 UTC130
}
8b8e83329a1datagen2026-10-12 12:01 UTC131
782069860236atagen2026-10-10 12:44 UTC132
impl Run {
782069860236atagen2026-10-10 12:44 UTC133
    pub(super) fn branch_offer(&self) -> Outcome {
8b8e83329a1datagen2026-10-12 12:01 UTC134
        let forum = self.path(&format!("/branches/{}", Branch::Forum.slug()));
8b8e83329a1datagen2026-10-12 12:01 UTC135
        let close = self.path(&format!("/branches/{}", Branch::CloseTab.slug()));
782069860236atagen2026-10-10 12:44 UTC136
        Outcome::json(
782069860236atagen2026-10-10 12:44 UTC137
            StatusCode::MULTIPLE_CHOICES,
782069860236atagen2026-10-10 12:44 UTC138
            json!({
782069860236atagen2026-10-10 12:44 UTC139
                "detail": "A browser tab is open. It has been open for some time.",
782069860236atagen2026-10-10 12:44 UTC140
                "choices": [
782069860236atagen2026-10-10 12:44 UTC141
                    { "href": forum, "method": "POST", "title": "Read the forum" },
782069860236atagen2026-10-10 12:44 UTC142
                    { "href": close, "method": "POST", "title": "Close the tab" },
782069860236atagen2026-10-10 12:44 UTC143
                ],
782069860236atagen2026-10-10 12:44 UTC144
            }),
782069860236atagen2026-10-10 12:44 UTC145
        )
782069860236atagen2026-10-10 12:44 UTC146
        .header(
782069860236atagen2026-10-10 12:44 UTC147
            header::LINK,
782069860236atagen2026-10-10 12:44 UTC148
            format!(
d46f9d0b953aatagen2026-10-10 14:41 UTC149
                "<{forum}>; rel=\"alternate\"; title=\"Read the forum\"; allow=\"POST\", <{close}>; rel=\"alternate\"; title=\"Close the tab\"; allow=\"POST\""
782069860236atagen2026-10-10 12:44 UTC150
            ),
782069860236atagen2026-10-10 12:44 UTC151
        )
782069860236atagen2026-10-10 12:44 UTC152
    }
782069860236atagen2026-10-10 12:44 UTC153
8b8e83329a1datagen2026-10-12 12:01 UTC154
    pub fn choose(&mut self, choice: Branch) -> Result<Outcome, Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC155
        if self.online.story != Story::Pending {
782069860236atagen2026-10-10 12:44 UTC156
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC157
                StatusCode::CONFLICT,
782069860236atagen2026-10-10 12:44 UTC158
                "no-decision-pending",
782069860236atagen2026-10-10 12:44 UTC159
                "There is no decision pending.",
782069860236atagen2026-10-10 12:44 UTC160
            ));
782069860236atagen2026-10-10 12:44 UTC161
        }
782069860236atagen2026-10-10 12:44 UTC162
        let (story, location, detail) = match choice {
8b8e83329a1datagen2026-10-12 12:01 UTC163
            Branch::Forum => {
8b8e83329a1datagen2026-10-12 12:01 UTC164
                self.standing.shame += 3;
880b29a0c11eatagen2026-10-13 09:27 UTC165
                let detail = if self.learn_meditation() {
880b29a0c11eatagen2026-10-13 09:27 UTC166
                    "The thread is from 2009. A technique called uli is discussed at length. Further down, someone explains how to breathe, and you write it down."
880b29a0c11eatagen2026-10-13 09:27 UTC167
                } else {
880b29a0c11eatagen2026-10-13 09:27 UTC168
                    "The thread is from 2009. A technique called uli is discussed at length."
880b29a0c11eatagen2026-10-13 09:27 UTC169
                };
0fdc47364c8catagen2026-10-11 04:45 UTC170
                (Story::ReadForum, self.path("/techniques"), detail)
782069860236atagen2026-10-10 12:44 UTC171
            }
8b8e83329a1datagen2026-10-12 12:01 UTC172
            Branch::CloseTab => {
8b8e83329a1datagen2026-10-12 12:01 UTC173
                self.standing.suspicion = 0;
782069860236atagen2026-10-10 12:44 UTC174
                (
782069860236atagen2026-10-10 12:44 UTC175
                    Story::ClosedTab,
782069860236atagen2026-10-10 12:44 UTC176
                    self.path(""),
782069860236atagen2026-10-10 12:44 UTC177
                    "The tab is closed. Nobody saw it.",
782069860236atagen2026-10-10 12:44 UTC178
                )
782069860236atagen2026-10-10 12:44 UTC179
            }
782069860236atagen2026-10-10 12:44 UTC180
        };
8b8e83329a1datagen2026-10-12 12:01 UTC181
        self.online.story = story;
782069860236atagen2026-10-10 12:44 UTC182
        self.touch();
782069860236atagen2026-10-10 12:44 UTC183
        Ok(
782069860236atagen2026-10-10 12:44 UTC184
            Outcome::json(StatusCode::SEE_OTHER, json!({ "detail": detail }))
782069860236atagen2026-10-10 12:44 UTC185
                .header(header::LOCATION, location),
782069860236atagen2026-10-10 12:44 UTC186
        )
782069860236atagen2026-10-10 12:44 UTC187
    }
782069860236atagen2026-10-10 12:44 UTC188
a6caa2f99786atagen2026-10-12 10:38 UTC189
    /// Re-reading is free. Each floor gets a couple of live reads that may surface the reply,
a6caa2f99786atagen2026-10-12 10:38 UTC190
    /// after which the forum is down and the browser serves what it kept.
b85a0d4536c3atagen2026-10-11 08:50 UTC191
    pub fn read_thread(&mut self) -> Result<Outcome, Problem> {
e807266c323batagen2026-10-12 09:46 UTC192
        if !self.thread_reachable() {
e807266c323batagen2026-10-12 09:46 UTC193
            return Err(Problem::new(
e807266c323batagen2026-10-12 09:46 UTC194
                StatusCode::NOT_FOUND,
e807266c323batagen2026-10-12 09:46 UTC195
                "no-thread",
a11497af08aaatagen2026-10-13 12:04 UTC196
                "There is no thread. Nobody has mentioned a forum yet.",
e807266c323batagen2026-10-12 09:46 UTC197
            ));
e807266c323batagen2026-10-12 09:46 UTC198
        }
3ac624339c98atagen2026-10-13 12:21 UTC199
        if self.online.story == Story::Pending {
3ac624339c98atagen2026-10-13 12:21 UTC200
            return Ok(thread(None));
3ac624339c98atagen2026-10-13 12:21 UTC201
        }
5cc6ab966f30atagen2026-10-11 11:23 UTC202
        self.end_kitchen_visit();
56d34031c321atagen2026-10-14 09:23 UTC203
        let rulerless = self.floor == Floor::Forum && !self.items.kit.ruler;
56d34031c321atagen2026-10-14 09:23 UTC204
        if self.online.uli_taught && !rulerless {
a11497af08aaatagen2026-10-13 12:04 UTC205
            return Ok(thread(None));
b85a0d4536c3atagen2026-10-11 08:50 UTC206
        }
880b29a0c11eatagen2026-10-13 09:27 UTC207
        if self.here.thread_rolls >= THREAD_READS {
8b8e83329a1datagen2026-10-12 12:01 UTC208
            let age = SystemTime::now()
8b8e83329a1datagen2026-10-12 12:01 UTC209
                .duration_since(*ELDER_POSTED)
a6caa2f99786atagen2026-10-12 10:38 UTC210
                .map_or(0, |age| age.as_secs());
a11497af08aaatagen2026-10-13 12:04 UTC211
            return Ok(thread(None)
a6caa2f99786atagen2026-10-12 10:38 UTC212
                .header(header::AGE, age.to_string())
a6caa2f99786atagen2026-10-12 10:38 UTC213
                .header(header::WARNING, r#"112 - "Disconnected operation""#));
a6caa2f99786atagen2026-10-12 10:38 UTC214
        }
a6caa2f99786atagen2026-10-12 10:38 UTC215
        self.here.thread_rolls += 1;
a6caa2f99786atagen2026-10-12 10:38 UTC216
        self.touch();
56d34031c321atagen2026-10-14 09:23 UTC217
        if rulerless
56d34031c321atagen2026-10-14 09:23 UTC218
            && self.chance(
56d34031c321atagen2026-10-14 09:23 UTC219
                self.decision(
56d34031c321atagen2026-10-14 09:23 UTC220
                    "printable-ruler",
56d34031c321atagen2026-10-14 09:23 UTC221
                    "Does this read surface a member's printable ruler?",
56d34031c321atagen2026-10-14 09:23 UTC222
                ),
56d34031c321atagen2026-10-14 09:23 UTC223
                PRINTABLE_RULER,
56d34031c321atagen2026-10-14 09:23 UTC224
            )
56d34031c321atagen2026-10-14 09:23 UTC225
        {
56d34031c321atagen2026-10-14 09:23 UTC226
            self.items.grant(Item::Ruler);
56d34031c321atagen2026-10-14 09:23 UTC227
            self.standing.shame += RULER_SHAME;
56d34031c321atagen2026-10-14 09:23 UTC228
            return Ok(thread(Some(
56d34031c321atagen2026-10-14 09:23 UTC229
                "A member has attached a printable ruler. Print at 100%. Do not scale to fit. You now know.",
56d34031c321atagen2026-10-14 09:23 UTC230
            ))
56d34031c321atagen2026-10-14 09:23 UTC231
            .header(header::LOCATION, self.path("/inventory/ruler")));
56d34031c321atagen2026-10-14 09:23 UTC232
        }
56d34031c321atagen2026-10-14 09:23 UTC233
        if self.online.uli_taught {
56d34031c321atagen2026-10-14 09:23 UTC234
            return Ok(thread(None));
56d34031c321atagen2026-10-14 09:23 UTC235
        }
880b29a0c11eatagen2026-10-13 09:27 UTC236
        let detail = if !self.online.meditation_taught {
880b29a0c11eatagen2026-10-13 09:27 UTC237
            if !self.learn_meditation() {
3ac624339c98atagen2026-10-13 12:21 UTC238
                return Ok(thread(None));
880b29a0c11eatagen2026-10-13 09:27 UTC239
            }
880b29a0c11eatagen2026-10-13 09:27 UTC240
            "Between two arguments about pumps, a reply explains how to breathe. In through the nose, out through the mouth. You write it down."
880b29a0c11eatagen2026-10-13 09:27 UTC241
        } else {
880b29a0c11eatagen2026-10-13 09:27 UTC242
            if !self.chance(
880b29a0c11eatagen2026-10-13 09:27 UTC243
                self.decision(
880b29a0c11eatagen2026-10-13 09:27 UTC244
                    "teach-uli",
a11497af08aaatagen2026-10-13 12:04 UTC245
                    "Does this read finally surface the 2009 reply that teaches uli?",
880b29a0c11eatagen2026-10-13 09:27 UTC246
                ),
880b29a0c11eatagen2026-10-13 09:27 UTC247
                ULI_ODDS,
880b29a0c11eatagen2026-10-13 09:27 UTC248
            ) {
3ac624339c98atagen2026-10-13 12:21 UTC249
                return Ok(thread(None));
880b29a0c11eatagen2026-10-13 09:27 UTC250
            }
880b29a0c11eatagen2026-10-13 09:27 UTC251
            self.online.uli_taught = true;
880b29a0c11eatagen2026-10-13 09:27 UTC252
            "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."
880b29a0c11eatagen2026-10-13 09:27 UTC253
        };
a11497af08aaatagen2026-10-13 12:04 UTC254
        Ok(thread(Some(detail)))
880b29a0c11eatagen2026-10-13 09:27 UTC255
    }
880b29a0c11eatagen2026-10-13 09:27 UTC256
880b29a0c11eatagen2026-10-13 09:27 UTC257
    fn learn_meditation(&mut self) -> bool {
880b29a0c11eatagen2026-10-13 09:27 UTC258
        let taught = self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC259
            self.decision(
880b29a0c11eatagen2026-10-13 09:27 UTC260
                "teach-meditation",
880b29a0c11eatagen2026-10-13 09:27 UTC261
                "Does the thread surface the reply that teaches the breathing technique?",
3f9091557ec3atagen2026-10-11 11:43 UTC262
            ),
880b29a0c11eatagen2026-10-13 09:27 UTC263
            MEDITATION_ODDS,
880b29a0c11eatagen2026-10-13 09:27 UTC264
        );
880b29a0c11eatagen2026-10-13 09:27 UTC265
        self.online.meditation_taught |= taught;
880b29a0c11eatagen2026-10-13 09:27 UTC266
        taught
b85a0d4536c3atagen2026-10-11 08:50 UTC267
    }
b85a0d4536c3atagen2026-10-11 08:50 UTC268
782069860236atagen2026-10-10 12:44 UTC269
    fn elder_strike(&mut self, problem: Problem) -> Problem {
782069860236atagen2026-10-10 12:44 UTC270
        self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC271
        let Elder::Unverified { strikes } = &mut self.online.elder else {
782069860236atagen2026-10-10 12:44 UTC272
            return problem;
782069860236atagen2026-10-10 12:44 UTC273
        };
782069860236atagen2026-10-10 12:44 UTC274
        *strikes += 1;
782069860236atagen2026-10-10 12:44 UTC275
        if *strikes < PATIENCE {
782069860236atagen2026-10-10 12:44 UTC276
            return problem;
782069860236atagen2026-10-10 12:44 UTC277
        }
782069860236atagen2026-10-10 12:44 UTC278
        self.end(Cause::Banned)
782069860236atagen2026-10-10 12:44 UTC279
    }
782069860236atagen2026-10-10 12:44 UTC280
782069860236atagen2026-10-10 12:44 UTC281
    /// Failed verification counts toward a ban even on GET, the way repeated bad credentials lock an account.
782069860236atagen2026-10-10 12:44 UTC282
    fn verify_member(&mut self, proxy_authorization: Option<&str>) -> Result<(), Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC283
        if !matches!(self.online.elder, Elder::Unverified { .. }) {
782069860236atagen2026-10-10 12:44 UTC284
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC285
        }
782069860236atagen2026-10-10 12:44 UTC286
        let Some(credentials) = proxy_authorization else {
782069860236atagen2026-10-10 12:44 UTC287
            return Err(verification("The forum requires verification."));
782069860236atagen2026-10-10 12:44 UTC288
        };
782069860236atagen2026-10-10 12:44 UTC289
        let Some(claimed) =
8b8e83329a1datagen2026-10-12 12:01 UTC290
            parse_credentials(credentials, Scheme::Ruler).and_then(|v| v.parse::<f64>().ok())
782069860236atagen2026-10-10 12:44 UTC291
        else {
782069860236atagen2026-10-10 12:44 UTC292
            return Err(verification("The forum requires verification, in inches."));
782069860236atagen2026-10-10 12:44 UTC293
        };
782069860236atagen2026-10-10 12:44 UTC294
        let actual = self.current_length();
782069860236atagen2026-10-10 12:44 UTC295
        if claimed >= actual + EXAGGERATION {
8b8e83329a1datagen2026-10-12 12:01 UTC296
            self.standing.shame += 2;
782069860236atagen2026-10-10 12:44 UTC297
            return Err(self.elder_strike(exaggeration()));
782069860236atagen2026-10-10 12:44 UTC298
        }
782069860236atagen2026-10-10 12:44 UTC299
        if !honest(claimed, actual) {
8b8e83329a1datagen2026-10-12 12:01 UTC300
            self.standing.shame += 1;
058650ec79d6atagen2026-10-12 10:49 UTC301
            let failed = verification("Verification failed. The forum can tell.");
058650ec79d6atagen2026-10-12 10:49 UTC302
            if self.girth_respected() {
058650ec79d6atagen2026-10-12 10:49 UTC303
                return Err(failed.header(
058650ec79d6atagen2026-10-12 10:49 UTC304
                    X_FORUM_NOTE,
058650ec79d6atagen2026-10-12 10:49 UTC305
                    "The mods have let this one go. Girth is also a dimension.",
058650ec79d6atagen2026-10-12 10:49 UTC306
                ));
058650ec79d6atagen2026-10-12 10:49 UTC307
            }
058650ec79d6atagen2026-10-12 10:49 UTC308
            return Err(self.elder_strike(failed));
782069860236atagen2026-10-10 12:44 UTC309
        }
8b8e83329a1datagen2026-10-12 12:01 UTC310
        self.online.elder = Elder::Verified;
782069860236atagen2026-10-10 12:44 UTC311
        self.touch();
782069860236atagen2026-10-10 12:44 UTC312
        Ok(())
782069860236atagen2026-10-10 12:44 UTC313
    }
782069860236atagen2026-10-10 12:44 UTC314
782069860236atagen2026-10-10 12:44 UTC315
    pub fn elder(
782069860236atagen2026-10-10 12:44 UTC316
        &mut self,
782069860236atagen2026-10-10 12:44 UTC317
        proxy_authorization: Option<&str>,
782069860236atagen2026-10-10 12:44 UTC318
        if_modified_since: Option<&str>,
782069860236atagen2026-10-10 12:44 UTC319
    ) -> Result<Outcome, Problem> {
782069860236atagen2026-10-10 12:44 UTC320
        if self.floor != Floor::Forum {
782069860236atagen2026-10-10 12:44 UTC321
            return Err(Problem::new(
782069860236atagen2026-10-10 12:44 UTC322
                StatusCode::NOT_FOUND,
782069860236atagen2026-10-10 12:44 UTC323
                "elder-absent",
782069860236atagen2026-10-10 12:44 UTC324
                "The Elder only posts on the forum.",
782069860236atagen2026-10-10 12:44 UTC325
            ));
782069860236atagen2026-10-10 12:44 UTC326
        }
782069860236atagen2026-10-10 12:44 UTC327
        self.verify_member(proxy_authorization)?;
782069860236atagen2026-10-10 12:44 UTC328
782069860236atagen2026-10-10 12:44 UTC329
        let current = if_modified_since
782069860236atagen2026-10-10 12:44 UTC330
            .and_then(|d| httpdate::parse_http_date(d).ok())
8b8e83329a1datagen2026-10-12 12:01 UTC331
            .is_some_and(|since| since >= *ELDER_POSTED);
782069860236atagen2026-10-10 12:44 UTC332
        if current {
8b8e83329a1datagen2026-10-12 12:01 UTC333
            if self.online.elder != Elder::Beaten {
8b8e83329a1datagen2026-10-12 12:01 UTC334
                self.online.elder = Elder::Beaten;
782069860236atagen2026-10-10 12:44 UTC335
                self.touch();
782069860236atagen2026-10-10 12:44 UTC336
            }
782069860236atagen2026-10-10 12:44 UTC337
            return Ok(Outcome::empty(StatusCode::NOT_MODIFIED)
782069860236atagen2026-10-10 12:44 UTC338
                .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED));
782069860236atagen2026-10-10 12:44 UTC339
        }
782069860236atagen2026-10-10 12:44 UTC340
        let spreadsheet = Outcome::raw(StatusCode::OK, "text/csv; header=present", SPREADSHEET)
782069860236atagen2026-10-10 12:44 UTC341
            .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED);
8b8e83329a1datagen2026-10-12 12:01 UTC342
        if self.online.filmed_at.is_some() {
782069860236atagen2026-10-10 12:44 UTC343
            return Ok(spreadsheet.header(X_FORUM_NOTE, "We have seen the footage."));
782069860236atagen2026-10-10 12:44 UTC344
        }
782069860236atagen2026-10-10 12:44 UTC345
        Ok(spreadsheet)
782069860236atagen2026-10-10 12:44 UTC346
    }
782069860236atagen2026-10-10 12:44 UTC347
}