Skip to main content

max / makenotwork

3.5 KB · 108 lines History Blame Raw
1 //! The creator-facing half of the monthly mail allowance: asking for more.
2 //!
3 //! The cap is a soft maximum, and a soft maximum without a way to ask is a hard
4 //! one with better manners. This is that way (`db::mail_caps`): a creator names
5 //! a number and says why, an operator decides, and a grant writes the
6 //! per-account override.
7 //!
8 //! The explanation is not a formality. It is what separates a growing list from
9 //! a stale one being blasted, which is the distinction the number alone cannot
10 //! carry.
11
12 use axum::{
13 Form,
14 extract::State,
15 response::{Html, IntoResponse, Response},
16 };
17 use serde::Deserialize;
18 use sqlx::PgPool;
19
20 use crate::{auth::AuthUser, db, error::Result, templates::FormStatusTemplate};
21
22 /// Form input for an allowance increase.
23 #[derive(Debug, Deserialize)]
24 pub(crate) struct MailCapRequestForm {
25 pub requested_cap: i32,
26 pub reason: String,
27 }
28
29 /// The smallest reason that can carry an argument. Shorter than this is a number
30 /// with no case attached, and the case is the whole point of asking.
31 const MIN_REASON_CHARS: usize = 20;
32
33 /// An upper bound on the ask, so a typo becomes a validation error rather than
34 /// an operator staring at a request for ten million.
35 const MAX_REQUESTED_CAP: i32 = 1_000_000;
36
37 /// Ask for a bigger monthly mail allowance.
38 #[tracing::instrument(skip_all, name = "users::mail_cap_request")]
39 pub(in crate::routes::api) async fn mail_cap_request(
40 State(db): State<PgPool>,
41 AuthUser(user): AuthUser,
42 Form(form): Form<MailCapRequestForm>,
43 ) -> Result<Response> {
44 user.check_not_sandbox()?;
45 user.check_not_suspended()?;
46
47 let refuse = |message: &str| -> Result<Response> {
48 Ok(Html(
49 FormStatusTemplate {
50 success: false,
51 message: message.to_string(),
52 }
53 .render_string()?,
54 )
55 .into_response())
56 };
57
58 if !user.can_create_projects {
59 return refuse("Creator access required");
60 }
61
62 let reason = form.reason.trim();
63 if reason.chars().count() < MIN_REASON_CHARS {
64 return refuse(
65 "Tell us what you are sending and roughly how often, so we can set a number that fits.",
66 );
67 }
68 if reason.chars().count() > 1000 {
69 return refuse("Keep the explanation under 1000 characters.");
70 }
71
72 if form.requested_cap <= 0 || form.requested_cap > MAX_REQUESTED_CAP {
73 return refuse("Ask for a number between 1 and 1,000,000 emails a month.");
74 }
75
76 // Asking for less than the current allowance is almost always a
77 // misunderstanding of what the number means, and granting it would lower
78 // the creator's own cap.
79 let current = db::mail_caps::effective_cap(&db, user.id).await?;
80 if i64::from(form.requested_cap) <= current {
81 return refuse(&format!(
82 "You already have {current} emails a month. Ask for a number above that."
83 ));
84 }
85
86 let filed = db::mail_caps::create_request(&db, user.id, form.requested_cap, reason).await?;
87 if !filed {
88 return refuse(
89 "You already have a request open. We will come back to you on that one rather than \
90 queueing a second.",
91 );
92 }
93
94 tracing::info!(
95 user_id = %user.id, requested_cap = form.requested_cap,
96 "creator asked for a higher monthly mail allowance"
97 );
98
99 Ok(Html(
100 FormStatusTemplate {
101 success: true,
102 message: "Request sent. We will come back to you.".to_string(),
103 }
104 .render_string()?,
105 )
106 .into_response())
107 }
108