use anyhow::Context; use axum::{ extract::{Query, State}, response::{IntoResponse, Json}, routing::get, Router, }; use diesel::{dsl::insert_into, prelude::*, update}; use serde::Deserialize; use serde_json::json; use uuid::Uuid; use crate::{ api_keys::ApiKey, app_error::AppError, app_state::{AppState, DbConn}, email::{MailSender as _, Mailer}, projects::Project, schema::{api_keys, projects}, settings::Settings, }; pub fn new_router(state: AppState) -> Router { Router::new().route("/say", get(say_get)).with_state(state) } #[derive(Deserialize)] struct SayQuery { key: Uuid, project: String, message: String, } async fn say_get( State(Settings { email: email_settings, .. }): State, State(mailer): State, DbConn(db_conn): DbConn, Query(query): Query, ) -> Result { let key = query.key.clone(); let maybe_api_key = db_conn .interact(move |conn| { update(api_keys::table.filter(ApiKey::with_id(key))) .set(api_keys::last_used_at.eq(diesel::dsl::now)) .returning(ApiKey::as_returning()) .get_result(conn) .optional() }) .await .unwrap() .context("unable to get API key")?; let api_key = match maybe_api_key { Some(api_key) => api_key, None => return Err(AppError::ForbiddenError("key not accepted".to_string())), }; let project_name = query.project.to_lowercase(); let selected_channels = db_conn .interact(move |conn| { insert_into(projects::table) .values(( projects::id.eq(Uuid::now_v7()), projects::team_id.eq(api_key.team_id.clone()), projects::name.eq(project_name.clone()), )) .on_conflict((projects::team_id, projects::name)) .do_nothing() .execute(conn) .context("failed to insert project")?; // It would be nice to merge these two database operations into one, // but it's not trivial to do so without faking an update; refer to: // https://stackoverflow.com/a/42217872 let project = Project::all() .filter(Project::with_team(api_key.team_id)) .filter(Project::with_name(project_name)) .first(conn) .context("failed to load project")?; project .selected_channels() .load(conn) .context("failed to load selected channels") }) .await .unwrap() .context("unable to get project")?; for channel in selected_channels { if let Some(email_data) = channel.email_data { if email_data.verified { let recipient: lettre::message::Mailbox = email_data.recipient.parse()?; let email = crate::email::Message { from: email_settings.message_from.clone().into(), to: recipient.into(), subject: "Shout".to_string(), text_body: query.message.clone(), }; tracing::info!("Sending email to recipient for channel {}", channel.id); mailer.send_batch(vec![email]).await?; } else { tracing::info!("Email recipient for channel {} is not verified", channel.id); } } } Ok(Json(json!({ "ok": true }))) }