atagen

jelq

public

an http based roguelike rpg about jelqing

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