an http based roguelike rpg about jelqing
Blame: src/game/encounters.rs
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 1 | use std::collections::HashMap; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 2 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 3 | use axum::http::{StatusCode, header}; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 4 | use rand::Rng; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 5 | use serde::Deserialize; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 6 | use serde_json::{Value, json}; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 7 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 8 | use super::{ |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 9 | Floor, Grip, Outcome, Run, SHOP, SUSPICION_LIMIT, X_DIAGNOSIS, honest, parse_credentials, |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 10 | round2, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 11 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 12 | use crate::problem::Problem; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 13 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 14 | pub const FORUM_THREAD: &str = "/forum/threads/2009"; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 15 | pub const ELDER_LAST_MODIFIED: &str = "Sun, 14 Jun 2009 03:12:00 GMT"; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 16 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 17 | pub const EXCUSES: [&str; 3] = ["changing", "homework", "pulled a muscle"]; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 18 | pub const THANKSGIVING_EXCUSES: [&str; 3] = ["on a call", "folding laundry", "praying"]; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 19 | const TRUTHS: [&str; 3] = ["the truth", "truth", "honesty"]; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 20 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 21 | const INTAKE: [&str; 3] = [ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 22 | "The clinic does not accept excuses.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 23 | "The clinic accepts one token. You have not been carrying it.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 24 | "Be honest.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 25 | ]; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 26 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 27 | const SPREADSHEET: &str = "\ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 28 | date,length,girth,technique,notes,honest? |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 29 | 2007-01-03,7.50,5.50,wet,baseline,no |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 30 | 2007-06-11,7.50,5.50,wet,feeling fuller,no |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 31 | 2008-01-02,7.50,5.50,dry,new routine,no |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 32 | 2008-09-19,7.50,5.50,uli,PUT is idempotent. do not use PUT.,no |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 33 | 2009-06-14,7.50,5.50,wet,progress is slow but real,no |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 34 | "; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 35 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 36 | pub fn forum_thread() -> Value { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 37 | json!({ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 38 | "title": "The definitive jelqing FAQ (updated 2009)", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 39 | "posts": [ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 40 | { "author": "TheElder", "body": "Warm up. Always warm up. A cold jelq is a 412 waiting to happen." }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 41 | { "author": "TheElder", "body": "Keep EQ between 50 and 70. Above 70 you are gambling. Above 90 you are a cautionary tale." }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 42 | { "author": "newbie_2009", "body": "I have been sending PUT for three weeks and nothing has changed." }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 43 | { "author": "TheElder", "body": "PUT is idempotent. Read the spec." }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 44 | { "author": "TheElder", "body": "Members verify with a Ruler. Proxy-Authorization, inches, no rounding up. We can tell." }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 45 | { "author": "throwaway_77", "body": "my doctor said i was normal. i didnt believe him. i still dont know what he meant by bearer" }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 46 | { "author": "TheElder", "body": "My numbers are in my spreadsheet. They have not changed since 2009. Nothing about me has changed since 2009." }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 47 | ], |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 48 | }) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 49 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 50 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 51 | #[derive(Deserialize)] |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 52 | pub struct PatchOp { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 53 | pub op: String, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 54 | pub path: String, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 55 | #[serde(default)] |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 56 | pub value: Value, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 57 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 58 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 59 | fn digest_params(authorization: &str) -> Option<HashMap<String, String>> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 60 | let (scheme, rest) = authorization.trim().split_once(' ')?; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 61 | if !scheme.eq_ignore_ascii_case("digest") { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 62 | return None; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 63 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 64 | Some( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 65 | rest.split(',') |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 66 | .filter_map(|pair| { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 67 | let (key, value) = pair.split_once('=')?; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 68 | Some(( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 69 | key.trim().to_ascii_lowercase(), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 70 | value.trim().trim_matches('"').to_owned(), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 71 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 72 | }) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 73 | .collect(), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 74 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 75 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 76 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 77 | fn exaggeration() -> Problem { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 78 | Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 79 | StatusCode::BAD_REQUEST, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 80 | "exaggeration", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 81 | "The server has measured. Lying is not a technique.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 82 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 83 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 84 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 85 | impl Run { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 86 | pub fn excuses(&self) -> Vec<&'static str> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 87 | if self.floor.number() < Floor::Grandmas.number() { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 88 | return EXCUSES.to_vec(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 89 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 90 | EXCUSES |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 91 | .into_iter() |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 92 | .chain(THANKSGIVING_EXCUSES) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 93 | .filter(|e| !self.heard.iter().any(|h| h == e)) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 94 | .collect() |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 95 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 96 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 97 | pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 98 | let digest = self.floor == Floor::Grandmas; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 99 | let challenge_value = if digest { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 100 | format!("Digest realm=\"household\", nonce=\"{}\"", self.nonce) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 101 | } else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 102 | "Excuse realm=\"household\"".to_owned() |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 103 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 104 | let challenge = |p: Problem| p.header(header::WWW_AUTHENTICATE, &challenge_value); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 105 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 106 | let Some(authorization) = authorization else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 107 | return Err(challenge( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 108 | Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 109 | StatusCode::UNAUTHORIZED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 110 | "someone-at-the-door", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 111 | format!("Someone is at the door. {}", self.mom_errand), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 112 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 113 | .with("inventory", format!("/runs/{}/inventory", self.id)), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 114 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 115 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 116 | |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 117 | let spoken = authorization |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 118 | .split_once(' ') |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 119 | .map(|(_, rest)| rest.trim().trim_matches('"').to_ascii_lowercase()) |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 120 | .unwrap_or_default(); |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 121 | if TRUTHS.contains(&spoken.as_str()) { |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 122 | self.shame += 10; |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 123 | self.leave_door(); |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 124 | return Ok(()); |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 125 | } |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 126 | if parse_credentials(authorization, "basic").is_some() { |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 127 | return Err(challenge(Problem::new( |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 128 | StatusCode::UNAUTHORIZED, |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 129 | "basic", |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 130 | "Mom does not accept Basic authentication. Nobody should.", |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 131 | ))); |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 132 | } |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 133 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 134 | let excuse = if digest { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 135 | let Some(params) = digest_params(authorization) else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 136 | return Err(challenge(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 137 | StatusCode::UNAUTHORIZED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 138 | "digest-required", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 139 | "Mom has started using Digest authentication.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 140 | ))); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 141 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 142 | if params.get("nonce") != Some(&self.nonce) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 143 | return Err(challenge(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 144 | StatusCode::UNAUTHORIZED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 145 | "stale-nonce", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 146 | "That was a different conversation.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 147 | ))); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 148 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 149 | params.get("username").cloned().unwrap_or_default() |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 150 | } else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 151 | parse_credentials(authorization, "excuse") |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 152 | .unwrap_or_default() |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 153 | .to_owned() |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 154 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 155 | let excuse = excuse.to_ascii_lowercase(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 156 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 157 | if TRUTHS.contains(&excuse.as_str()) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 158 | self.shame += 10; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 159 | self.leave_door(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 160 | return Ok(()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 161 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 162 | if digest && self.heard.contains(&excuse) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 163 | self.shame += 3; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 164 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 165 | return Err(challenge(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 166 | StatusCode::UNAUTHORIZED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 167 | "excuse-replayed", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 168 | "Mom has heard that one before. She would have preferred the truth.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 169 | ))); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 170 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 171 | if self.excuses().contains(&excuse.as_str()) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 172 | self.heard.push(excuse); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 173 | self.leave_door(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 174 | return Ok(()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 175 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 176 | self.shame += 3; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 177 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 178 | let detail = if digest { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 179 | "Mom has not heard that one before. She does not believe it. She would have preferred the truth." |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 180 | } else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 181 | "Mom has not heard that one before. She does not believe it." |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 182 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 183 | Err(challenge(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 184 | StatusCode::UNAUTHORIZED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 185 | "excuse-rejected", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 186 | detail, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 187 | ))) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 188 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 189 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 190 | pub(super) fn errand(&mut self, summoned: bool) -> &'static str { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 191 | if summoned { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 192 | return "She heard a noise."; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 193 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 194 | let errands: &[&'static str] = if self.floor == Floor::Grandmas { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 195 | &[ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 196 | "Grandma would like to see you.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 197 | "Dinner is in ten minutes.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 198 | "She needs help with the folding table.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 199 | "An uncle has asked where you went.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 200 | ] |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 201 | } else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 202 | &[ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 203 | "She has brought laundry.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 204 | "She wants to know if you have eaten.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 205 | "She needs the wifi password.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 206 | "She would like to talk about your future.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 207 | ] |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 208 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 209 | errands[self.rng.random_range(0..errands.len())] |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 210 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 211 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 212 | fn leave_door(&mut self) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 213 | self.mom_present = false; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 214 | self.suspicion = 0; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 215 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 216 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 217 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 218 | pub(super) fn floor_enemy(&mut self) -> Result<(), Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 219 | let wanders_in = matches!(self.floor, Floor::Bathroom | Floor::Hotel) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 220 | && self.rng.random_ratio(u32::from(self.suspicion) + 1, 32); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 221 | if self.suspicion < SUSPICION_LIMIT && !wanders_in { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 222 | return Ok(()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 223 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 224 | match self.floor { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 225 | Floor::Bathroom => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 226 | self.suspicion = 0; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 227 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 228 | if self.items.door_locks > 0 { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 229 | self.items.door_locks -= 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 230 | return Ok(()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 231 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 232 | self.roommate_told = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 233 | Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 234 | StatusCode::CONFLICT, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 235 | "roommate", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 236 | "The roommate needs the bathroom. He will mention this to people.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 237 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 238 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 239 | Floor::LockerRoom => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 240 | self.suspicion = 0; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 241 | self.shame += 5; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 242 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 243 | Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 244 | StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 245 | "gym-staff", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 246 | "Gym staff have been notified. The server cannot serve this request for legal reasons.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 247 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 248 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 249 | Floor::Hotel => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 250 | self.suspicion = 0; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 251 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 252 | if self.items.dnd_sign { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 253 | return Ok(()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 254 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 255 | self.shame += 3; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 256 | Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 257 | StatusCode::UNAUTHORIZED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 258 | "housekeeping", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 259 | "Housekeeping has entered. Housekeeping accepts no excuses.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 260 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 261 | .header(header::WWW_AUTHENTICATE, "Excuse realm=\"housekeeping\"")) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 262 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 263 | _ => Ok(()), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 264 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 265 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 266 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 267 | pub fn search_drawer(&mut self) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 268 | self.drawer_searches += 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 269 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 270 | let first = self.drawer_searches == 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 271 | let id = self.id.clone(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 272 | let found = |slug: &str, detail: &str| { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 273 | Outcome::json(StatusCode::FOUND, json!({ "detail": detail })) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 274 | .header(header::LOCATION, format!("/runs/{id}/inventory/{slug}")) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 275 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 276 | match self.floor { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 277 | Floor::Bedroom if first && !self.items.ruler => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 278 | self.items.ruler = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 279 | self.shame += 10; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 280 | Ok(found( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 281 | "ruler", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 282 | "You now know. This is the first honest measurement of the run.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 283 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 284 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 285 | Floor::Bathroom if first => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 286 | self.items.door_locks += 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 287 | Ok(found("door-lock", "A door lock. It works once.")) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 288 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 289 | Floor::LockerRoom => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 290 | self.shame += 3; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 291 | Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 292 | StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 293 | "not-your-locker", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 294 | "That locker belongs to Greg. The server cannot serve this request for legal reasons.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 295 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 296 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 297 | Floor::Grandmas if first => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 298 | self.items.cage = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 299 | self.shame += 5; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 300 | Ok(found( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 301 | "chastity-cage", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 302 | "A small box. Inside was a cage. It is now on. The key is not in the box.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 303 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 304 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 305 | Floor::Grandmas if self.items.cage => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 306 | self.items.cage = false; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 307 | Ok(Outcome::json( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 308 | StatusCode::OK, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 309 | json!({ "detail": "The key was under where the box had been. The cage is off." }), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 310 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 311 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 312 | Floor::Hotel if first => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 313 | self.items.dnd_sign = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 314 | self.items.ice_packs += 2; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 315 | Ok(found( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 316 | "do-not-disturb-sign", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 317 | "A Do Not Disturb sign, and two ice packs from the machine down the hall.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 318 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 319 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 320 | Floor::Forum if first => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 321 | self.items.spreadsheet = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 322 | Ok(found( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 323 | "spreadsheet", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 324 | "Your own spreadsheet. Every line is flat.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 325 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 326 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 327 | Floor::Clinic => Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 328 | StatusCode::FORBIDDEN, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 329 | "doctors-drawer", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 330 | "That is the doctor's drawer.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 331 | )), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 332 | _ => Ok(Outcome::empty(StatusCode::NO_CONTENT)), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 333 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 334 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 335 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 336 | pub fn shop_view(&self) -> Result<Value, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 337 | if !self.floor.shop_open() { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 338 | return Err(pump_bro_absent()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 339 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 340 | let stock: Vec<Value> = SHOP |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 341 | .iter() |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 342 | .map(|item| { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 343 | json!({ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 344 | "item": item.slug, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 345 | "price": item.price, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 346 | "description": item.description, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 347 | "href": format!("/runs/{}/shop/{}", self.id, item.slug), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 348 | "method": "POST", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 349 | }) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 350 | }) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 351 | .collect(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 352 | Ok(json!({ "merchant": "Pump-Bro", "cash": self.cash, "stock": stock })) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 353 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 354 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 355 | pub fn buy(&mut self, slug: &str) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 356 | if !self.floor.shop_open() { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 357 | return Err(pump_bro_absent()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 358 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 359 | let Some(item) = SHOP.iter().find(|i| i.slug == slug) else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 360 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 361 | StatusCode::NOT_FOUND, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 362 | "not-for-sale", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 363 | "Pump-Bro does not sell that. He knows a guy.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 364 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 365 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 366 | if slug == "pump" { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 367 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 368 | StatusCode::UPGRADE_REQUIRED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 369 | "upsell", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 370 | "This pump requires an upgrade.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 371 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 372 | .header(header::UPGRADE, "pump-pro/2.0") |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 373 | .with("upgrade", format!("/runs/{}/shop/pump-pro", self.id))); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 374 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 375 | if self.cash < item.price { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 376 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 377 | StatusCode::PAYMENT_REQUIRED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 378 | "insufficient-funds", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 379 | "Payment is required. RFC 9110 has reserved this status for future use since 1997. The future is here and you cannot afford it.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 380 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 381 | .with("cash", self.cash) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 382 | .with("price", item.price)); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 383 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 384 | self.cash -= item.price; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 385 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 386 | let items = &mut self.items; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 387 | match slug { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 388 | "supplement" => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 389 | items.supplements += 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 390 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 391 | StatusCode::GATEWAY_TIMEOUT, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 392 | "still-shipping", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 393 | "The supplement is still shipping. You have been charged. It may arrive.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 394 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 395 | .with("cash", self.cash)); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 396 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 397 | "pump-pro" => items.pump_pro = true, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 398 | "extender" => items.extender = true, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 399 | "cock-ring" => items.ring = true, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 400 | "door-lock" => items.door_locks += 1, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 401 | "do-not-disturb-sign" => items.dnd_sign = true, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 402 | "ruler" => items.ruler = true, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 403 | "ice-pack" => items.ice_packs += 1, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 404 | _ => items.cage = false, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 405 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 406 | Ok(Outcome::json( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 407 | StatusCode::CREATED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 408 | json!({ "detail": "Purchased. No refunds.", "cash": self.cash }), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 409 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 410 | .header( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 411 | header::LOCATION, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 412 | format!("/runs/{}/inventory/{slug}", self.id), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 413 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 414 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 415 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 416 | pub fn drop_item(&mut self, slug: &str) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 417 | match slug { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 418 | "extender" => self.items.extender = false, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 419 | "cock-ring" => self.items.ring = false, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 420 | "chastity-cage" => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 421 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 422 | StatusCode::LOCKED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 423 | "caged", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 424 | "The cage is locked.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 425 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 426 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 427 | _ => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 428 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 429 | StatusCode::FORBIDDEN, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 430 | "cannot-be-dropped", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 431 | "That cannot be dropped.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 432 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 433 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 434 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 435 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 436 | Ok(Outcome::empty(StatusCode::NO_CONTENT)) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 437 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 438 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 439 | pub(super) fn branch_offer(&self) -> Outcome { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 440 | let forum = format!("/runs/{}/branches/forum", self.id); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 441 | let close = format!("/runs/{}/branches/close-tab", self.id); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 442 | Outcome::json( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 443 | StatusCode::MULTIPLE_CHOICES, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 444 | json!({ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 445 | "detail": "A browser tab is open. It has been open for some time.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 446 | "choices": [ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 447 | { "href": forum, "method": "POST", "title": "Read the forum" }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 448 | { "href": close, "method": "POST", "title": "Close the tab" }, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 449 | ], |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 450 | }), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 451 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 452 | .header( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 453 | header::LINK, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 454 | format!( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 455 | "<{forum}>; rel=\"alternate\"; title=\"Read the forum\", <{close}>; rel=\"alternate\"; title=\"Close the tab\"" |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 456 | ), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 457 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 458 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 459 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 460 | pub fn choose(&mut self, choice: &str) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 461 | if !self.branch_pending { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 462 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 463 | StatusCode::CONFLICT, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 464 | "no-decision-pending", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 465 | "There is no decision pending.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 466 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 467 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 468 | let (location, detail) = match choice { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 469 | "forum" => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 470 | self.forum_read = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 471 | self.shame += 3; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 472 | ( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 473 | format!("/runs/{}/techniques", self.id), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 474 | "The thread is from 2009. A technique called uli is discussed at length.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 475 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 476 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 477 | "close-tab" => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 478 | self.suspicion = 0; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 479 | ( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 480 | format!("/runs/{}", self.id), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 481 | "The tab is closed. Nobody saw it.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 482 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 483 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 484 | _ => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 485 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 486 | StatusCode::NOT_FOUND, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 487 | "no-such-choice", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 488 | "That was not one of the choices.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 489 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 490 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 491 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 492 | self.branch_pending = false; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 493 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 494 | Ok( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 495 | Outcome::json(StatusCode::SEE_OTHER, json!({ "detail": detail })) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 496 | .header(header::LOCATION, location), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 497 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 498 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 499 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 500 | pub fn put(&mut self) -> Outcome { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 501 | if self.put_applied { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 502 | return Outcome::empty(StatusCode::NO_CONTENT); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 503 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 504 | self.put_applied = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 505 | self.length += 0.01; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 506 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 507 | Outcome::json(StatusCode::OK, json!({ "detail": "Applied." })) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 508 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 509 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 510 | pub fn patch(&mut self, ops: &[PatchOp]) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 511 | for op in ops { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 512 | if op.op != "test" { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 513 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 514 | StatusCode::UNPROCESSABLE_ENTITY, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 515 | "wishful-thinking", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 516 | "The server accepts only the truth.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 517 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 518 | .with("op", op.op.clone())); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 519 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 520 | let actual = match op.path.as_str() { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 521 | "/length" => self.current_length(), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 522 | "/girth" => self.girth, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 523 | "/foreskin" => self.foreskin.unwrap_or(0.0), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 524 | _ => { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 525 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 526 | StatusCode::UNPROCESSABLE_ENTITY, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 527 | "no-such-measurement", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 528 | "There is no such measurement.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 529 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 530 | .with("path", op.path.clone())); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 531 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 532 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 533 | let Some(claimed) = op.value.as_f64() else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 534 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 535 | StatusCode::BAD_REQUEST, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 536 | "malformed-patch", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 537 | "A test requires a numeric value.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 538 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 539 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 540 | if op.path == "/length" && claimed >= actual + 1.0 { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 541 | self.shame += 2; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 542 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 543 | return Err(exaggeration()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 544 | } |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 545 | if !honest(claimed, actual) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 546 | self.shame += 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 547 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 548 | return Err( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 549 | Problem::new(StatusCode::CONFLICT, "test-failed", "The test failed.") |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 550 | .with("path", op.path.clone()), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 551 | ); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 552 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 553 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 554 | Ok(Outcome::json( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 555 | StatusCode::OK, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 556 | json!({ "detail": "Correct." }), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 557 | )) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 558 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 559 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 560 | pub fn delete_foreskin(&mut self, if_match: Option<&str>) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 561 | if self.foreskin.is_none() { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 562 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 563 | StatusCode::GONE, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 564 | "foreskin-gone", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 565 | "The foreskin is gone. It will not be returning.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 566 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 567 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 568 | let Some(if_match) = if_match else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 569 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 570 | StatusCode::PRECONDITION_REQUIRED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 571 | "confirmation-required", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 572 | "The server requires you to be sure. Send If-Match.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 573 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 574 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 575 | if !super::etag_matches(if_match, &self.etag()) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 576 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 577 | StatusCode::PRECONDITION_FAILED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 578 | "stale-foreskin", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 579 | "The foreskin has changed since you last looked at it.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 580 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 581 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 582 | self.foreskin = None; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 583 | self.shame += 20; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 584 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 585 | Ok(Outcome::empty(StatusCode::NO_CONTENT)) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 586 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 587 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 588 | pub fn set_grip(&mut self, grip: Grip) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 589 | self.grip = grip; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 590 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 591 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 592 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 593 | pub fn accept_terms(&mut self) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 594 | if self.floor != Floor::Hotel { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 595 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 596 | StatusCode::NOT_FOUND, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 597 | "no-portal", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 598 | "There is no captive portal here. There is only the regular kind of captivity.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 599 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 600 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 601 | self.wifi_accepted = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 602 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 603 | Ok(Outcome::json( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 604 | StatusCode::SEE_OTHER, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 605 | json!({ "detail": "Connected. The hotel does not want to know." }), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 606 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 607 | .header(header::LOCATION, format!("/runs/{}", self.id))) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 608 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 609 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 610 | pub fn elder( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 611 | &mut self, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 612 | proxy_authorization: Option<&str>, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 613 | if_modified_since: Option<&str>, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 614 | ) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 615 | if self.floor != Floor::Forum { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 616 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 617 | StatusCode::NOT_FOUND, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 618 | "elder-absent", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 619 | "The Elder only posts on the forum.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 620 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 621 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 622 | if !self.verified { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 623 | let verification = |detail: &str| { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 624 | Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 625 | StatusCode::PROXY_AUTHENTICATION_REQUIRED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 626 | "verification-required", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 627 | detail, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 628 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 629 | .header(header::PROXY_AUTHENTICATE, "Ruler realm=\"forum\"") |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 630 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 631 | let Some(credentials) = proxy_authorization else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 632 | return Err(verification("The forum requires verification.")); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 633 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 634 | let Some(claimed) = |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 635 | parse_credentials(credentials, "ruler").and_then(|v| v.parse::<f64>().ok()) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 636 | else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 637 | return Err(verification("The forum requires verification, in inches.")); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 638 | }; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 639 | let actual = self.current_length(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 640 | if claimed >= actual + 1.0 { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 641 | self.shame += 2; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 642 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 643 | return Err(exaggeration()); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 644 | } |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 645 | if !honest(claimed, actual) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 646 | self.shame += 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 647 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 648 | return Err(verification("Verification failed. The forum can tell.")); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 649 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 650 | self.verified = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 651 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 652 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 653 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 654 | let last_modified = |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 655 | httpdate::parse_http_date(ELDER_LAST_MODIFIED).expect("valid constant date"); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 656 | let current = if_modified_since |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 657 | .and_then(|d| httpdate::parse_http_date(d).ok()) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 658 | .is_some_and(|since| since >= last_modified); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 659 | if current { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 660 | self.elder_beaten = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 661 | self.touch(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 662 | return Ok(Outcome::empty(StatusCode::NOT_MODIFIED) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 663 | .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED)); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 664 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 665 | Ok( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 666 | Outcome::raw(StatusCode::OK, "text/csv; header=present", SPREADSHEET) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 667 | .header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 668 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 669 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 670 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 671 | pub fn diagnose( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 672 | &mut self, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 673 | authorization: Option<&str>, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 674 | if_none_match: Option<&str>, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 675 | ) -> Result<Outcome, Problem> { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 676 | if self.floor != Floor::Clinic { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 677 | let floors = Floor::Clinic.number() - self.floor.number(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 678 | return Err(Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 679 | StatusCode::TOO_EARLY, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 680 | "no-appointment", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 681 | format!("The clinic's next available appointment is {floors} floors away."), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 682 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 683 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 684 | let intake = Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 685 | StatusCode::UNAUTHORIZED, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 686 | "intake", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 687 | "The clinic requires authorization.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 688 | ); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 689 | let Some(authorization) = authorization else { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 690 | return Err(intake.header(header::WWW_AUTHENTICATE, "Bearer realm=\"clinic\"")); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 691 | }; |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 692 | let truthful = parse_credentials(authorization, "bearer") |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 693 | .is_some_and(|token| token.eq_ignore_ascii_case("honesty")); |
177535ac8abf | atagen | 2026-10-10 10:39 UTC | 694 | if !truthful { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 695 | let hint = INTAKE[self.intake_attempts.min(INTAKE.len() - 1)]; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 696 | self.intake_attempts += 1; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 697 | return Err(intake.header( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 698 | header::WWW_AUTHENTICATE, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 699 | format!( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 700 | "Bearer realm=\"clinic\", error=\"invalid_token\", error_description=\"{hint}\"" |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 701 | ), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 702 | )); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 703 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 704 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 705 | self.peyronie = false; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 706 | let etag = self.origin_etag.clone(); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 707 | if if_none_match.is_some_and(|inm| super::etag_matches(inm, &etag)) { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 708 | self.cured = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 709 | return Ok(Outcome::empty(StatusCode::NOT_MODIFIED) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 710 | .header(header::ETAG, etag) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 711 | .header(X_DIAGNOSIS, "normal")); |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 712 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 713 | if !self.measured { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 714 | self.measured = true; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 715 | if !self.items.ruler { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 716 | self.shame += 15; |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 717 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 718 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 719 | Ok(Outcome::json( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 720 | StatusCode::OK, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 721 | json!({ |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 722 | "diagnosis": "within normal range", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 723 | "measured_length": round2(self.length), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 724 | "you_said": round2(self.self_reported()), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 725 | "prescription": "Please stop.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 726 | }), |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 727 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 728 | .header(header::ETAG, etag)) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 729 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 730 | } |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 731 | |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 732 | fn pump_bro_absent() -> Problem { |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 733 | Problem::new( |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 734 | StatusCode::NOT_FOUND, |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 735 | "pump-bro-absent", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 736 | "Pump-Bro is not here.", |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 737 | ) |
dfa38cf4cd4b | atagen | 2026-10-10 09:17 UTC | 738 | } |