shoutdotdev/src/v0_router.rs

110 lines
3.6 KiB
Rust
Raw Normal View History

2025-01-31 14:30:08 -08:00
use anyhow::Context;
use axum::{
2025-02-16 13:06:38 -08:00
extract::{Query, State},
2025-01-31 14:30:08 -08:00
response::{IntoResponse, Json},
routing::get,
Router,
};
use diesel::{dsl::insert_into, prelude::*, update};
2025-02-16 13:06:38 -08:00
use lettre::Transport as _;
use serde::Deserialize;
use serde_json::json;
2025-01-31 14:30:08 -08:00
use uuid::Uuid;
use crate::{
api_keys::ApiKey,
app_error::AppError,
2025-02-16 13:06:38 -08:00
app_state::{AppState, DbConn, Mailer},
2025-01-31 14:30:08 -08:00
projects::Project,
2025-02-16 13:06:38 -08:00
schema::{api_keys, projects},
settings::Settings,
2025-01-31 14:30:08 -08:00
};
pub fn new_router(state: AppState) -> Router<AppState> {
Router::new().route("/say", get(say_get)).with_state(state)
}
#[derive(Deserialize)]
struct SayQuery {
key: Uuid,
project: String,
message: String,
}
async fn say_get(
2025-02-16 13:06:38 -08:00
State(Settings {
email: email_settings,
..
}): State<Settings>,
State(Mailer(mailer)): State<Mailer>,
2025-01-31 14:30:08 -08:00
DbConn(db_conn): DbConn,
Query(query): Query<SayQuery>,
) -> Result<impl IntoResponse, AppError> {
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();
2025-02-16 13:06:38 -08:00
let selected_channels = db_conn
2025-01-31 14:30:08 -08:00
.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()
2025-02-16 13:06:38 -08:00
.execute(conn)
.context("failed to insert project")?;
2025-01-31 14:30:08 -08:00
// 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
2025-02-16 13:06:38 -08:00
let project = Project::all()
2025-01-31 14:30:08 -08:00
.filter(Project::with_team(api_key.team_id))
.filter(Project::with_name(project_name))
.first(conn)
2025-02-16 13:06:38 -08:00
.context("failed to load project")?;
project
.selected_channels()
.load(conn)
.context("failed to load selected channels")
2025-01-31 14:30:08 -08:00
})
.await
.unwrap()
.context("unable to get project")?;
2025-02-16 13:06:38 -08:00
for channel in selected_channels {
if let Some(email_data) = channel.email_data {
if email_data.verified {
let recipient: lettre::Address = email_data.recipient.parse()?;
let email = lettre::Message::builder()
.from(email_settings.message_from.clone().into())
.reply_to(email_settings.message_from.clone().into())
.to(recipient.into())
.subject("Shout")
.header(lettre::message::header::ContentType::TEXT_PLAIN)
.body(query.message.clone())?;
tracing::info!("Sending email to recipient for channel {}", channel.id);
mailer.send(&email)?;
} else {
tracing::info!("Email recipient for channel {} is not verified", channel.id);
}
}
2025-01-31 14:30:08 -08:00
}
2025-02-16 13:06:38 -08:00
Ok(Json(json!({ "ok": true })))
2025-01-31 14:30:08 -08:00
}