147 lines
4.4 KiB
Rust
147 lines
4.4 KiB
Rust
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 std::fmt::Display;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct BodyData {
|
|
title: String,
|
|
html: String,
|
|
text: String,
|
|
idempotency_key: String,
|
|
}
|
|
|
|
#[tracing::instrument(name = "Creating newsletter isue", skip_all, fields(issue_id = tracing::field::Empty))]
|
|
pub async fn insert_newsletter_issue(
|
|
transaction: &mut Transaction<'static, Postgres>,
|
|
title: &str,
|
|
email_template: &dyn EmailTemplate,
|
|
) -> Result<Uuid, sqlx::Error> {
|
|
let newsletter_issue_id = Uuid::new_v4();
|
|
tracing::Span::current().record("issue_id", newsletter_issue_id.to_string());
|
|
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)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
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(name = "Adding new task to queue", skip(transaction))]
|
|
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_all, fields(title = %form.title))]
|
|
pub async fn publish_newsletter(
|
|
State(AppState {
|
|
connection_pool,
|
|
base_url,
|
|
..
|
|
}): State<AppState>,
|
|
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
|
Form(form): Form<BodyData>,
|
|
) -> Result<Response, AppError> {
|
|
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<(), anyhow::Error> {
|
|
if form.title.is_empty() {
|
|
anyhow::bail!("The title was empty.");
|
|
}
|
|
if form.html.is_empty() || form.text.is_empty() {
|
|
anyhow::bail!("The content was empty.");
|
|
}
|
|
Ok(())
|
|
}
|