atagen

jelq

public

an http based roguelike rpg about jelqing

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