//! The creator-facing half of the monthly mail allowance: asking for more. //! //! The cap is a soft maximum, and a soft maximum without a way to ask is a hard //! one with better manners. This is that way (`db::mail_caps`): a creator names //! a number and says why, an operator decides, and a grant writes the //! per-account override. //! //! The explanation is not a formality. It is what separates a growing list from //! a stale one being blasted, which is the distinction the number alone cannot //! carry. use axum::{ Form, extract::State, response::{Html, IntoResponse, Response}, }; use serde::Deserialize; use sqlx::PgPool; use crate::{auth::AuthUser, db, error::Result, templates::FormStatusTemplate}; /// Form input for an allowance increase. #[derive(Debug, Deserialize)] pub(crate) struct MailCapRequestForm { pub requested_cap: i32, pub reason: String, } /// The smallest reason that can carry an argument. Shorter than this is a number /// with no case attached, and the case is the whole point of asking. const MIN_REASON_CHARS: usize = 20; /// An upper bound on the ask, so a typo becomes a validation error rather than /// an operator staring at a request for ten million. const MAX_REQUESTED_CAP: i32 = 1_000_000; /// Ask for a bigger monthly mail allowance. #[tracing::instrument(skip_all, name = "users::mail_cap_request")] pub(in crate::routes::api) async fn mail_cap_request( State(db): State, AuthUser(user): AuthUser, Form(form): Form, ) -> Result { user.check_not_sandbox()?; user.check_not_suspended()?; let refuse = |message: &str| -> Result { Ok(Html( FormStatusTemplate { success: false, message: message.to_string(), } .render_string()?, ) .into_response()) }; if !user.can_create_projects { return refuse("Creator access required"); } let reason = form.reason.trim(); if reason.chars().count() < MIN_REASON_CHARS { return refuse( "Tell us what you are sending and roughly how often, so we can set a number that fits.", ); } if reason.chars().count() > 1000 { return refuse("Keep the explanation under 1000 characters."); } if form.requested_cap <= 0 || form.requested_cap > MAX_REQUESTED_CAP { return refuse("Ask for a number between 1 and 1,000,000 emails a month."); } // Asking for less than the current allowance is almost always a // misunderstanding of what the number means, and granting it would lower // the creator's own cap. let current = db::mail_caps::effective_cap(&db, user.id).await?; if i64::from(form.requested_cap) <= current { return refuse(&format!( "You already have {current} emails a month. Ask for a number above that." )); } let filed = db::mail_caps::create_request(&db, user.id, form.requested_cap, reason).await?; if !filed { return refuse( "You already have a request open. We will come back to you on that one rather than \ queueing a second.", ); } tracing::info!( user_id = %user.id, requested_cap = form.requested_cap, "creator asked for a higher monthly mail allowance" ); Ok(Html( FormStatusTemplate { success: true, message: "Request sent. We will come back to you.".to_string(), } .render_string()?, ) .into_response()) }