use std::fmt::Display; use crate::{ authentication::AuthenticatedUser, idempotency::{IdempotencyKey, save_response, try_processing}, routes::{AdminError, AppError}, startup::AppState, templates::{EmailTemplate, MessageTemplate, StandaloneEmailTemplate}, }; use anyhow::Context; use askama::Template; use axum::{ Extension, Form, extract::State, response::{Html, IntoResponse, Response}, }; use sqlx::{Executor, Postgres, Transaction}; use uuid::Uuid; #[derive(serde::Deserialize)] pub struct BodyData { title: String, html: String, text: String, idempotency_key: String, } #[tracing::instrument(skip_all)] pub async fn insert_newsletter_issue( transaction: &mut Transaction<'static, Postgres>, title: &str, email_template: &dyn EmailTemplate, ) -> Result { let newsletter_issue_id = Uuid::new_v4(); let query = sqlx::query!( r#" INSERT INTO newsletter_issues ( newsletter_issue_id, title, text_content, html_content, published_at ) VALUES ($1, $2, $3, $4, now()) "#, newsletter_issue_id, title, email_template.text(), email_template.html(), ); transaction.execute(query).await?; Ok(newsletter_issue_id) } pub enum EmailType { NewPost, Newsletter, } impl Display for EmailType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EmailType::NewPost => write!(f, "new_post"), EmailType::Newsletter => write!(f, "newsletter"), } } } #[tracing::instrument(skip_all)] pub async fn enqueue_delivery_tasks( transaction: &mut Transaction<'static, Postgres>, newsletter_issue_id: Uuid, kind: EmailType, ) -> Result<(), sqlx::Error> { let query = sqlx::query!( r#" INSERT INTO issue_delivery_queue ( newsletter_issue_id, subscriber_email, unsubscribe_token, kind ) SELECT $1, email, unsubscribe_token, $2 FROM subscriptions WHERE status = 'confirmed' "#, newsletter_issue_id, kind.to_string() ); transaction.execute(query).await?; Ok(()) } #[tracing::instrument(name = "Publishing a newsletter", skip(connection_pool, form))] pub async fn publish_newsletter( State(AppState { connection_pool, base_url, .. }): State, Extension(AuthenticatedUser { user_id, .. }): Extension, Form(form): Form, ) -> Result { validate_form(&form).map_err(|e| AdminError::Publish(anyhow::anyhow!(e)))?; let idempotency_key: IdempotencyKey = form .idempotency_key .try_into() .map_err(AdminError::Idempotency)?; let mut transaction = match try_processing(&connection_pool, &idempotency_key, user_id).await? { crate::idempotency::NextAction::StartProcessing(t) => t, crate::idempotency::NextAction::ReturnSavedResponse(response) => { return Ok(response); } }; let email_template = StandaloneEmailTemplate { base_url: &base_url, text_content: &form.text, html_content: &form.html, }; let issue_id = insert_newsletter_issue(&mut transaction, &form.title, &email_template) .await .context("Failed to store newsletter issue details.")?; enqueue_delivery_tasks(&mut transaction, issue_id, EmailType::Newsletter) .await .context("Failed to enqueue delivery tasks.")?; let message = String::from("Your email has been queued for delivery."); let template = MessageTemplate::Success { message }; let response = Html(template.render().unwrap()).into_response(); let response = save_response(transaction, &idempotency_key, user_id, response) .await .map_err(AdminError::UnexpectedError)?; Ok(response) } fn validate_form(form: &BodyData) -> Result<(), &'static str> { if form.title.is_empty() { return Err("The title was empty."); } if form.html.is_empty() || form.text.is_empty() { return Err("The content was empty."); } Ok(()) }