mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: direct email notification endpoint
This commit is contained in:
@@ -129,12 +129,11 @@ impl NotificationBuilder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_many(
|
async fn insert_many_records(
|
||||||
&self,
|
&self,
|
||||||
users: Vec<DBUserId>,
|
users: &[DBUserId],
|
||||||
transaction: &mut PgTransaction<'_>,
|
transaction: &mut PgTransaction<'_>,
|
||||||
redis: &RedisPool,
|
) -> Result<Vec<i64>, DatabaseError> {
|
||||||
) -> Result<(), DatabaseError> {
|
|
||||||
let notification_ids =
|
let notification_ids =
|
||||||
generate_many_notification_ids(users.len(), &mut *transaction)
|
generate_many_notification_ids(users.len(), &mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -163,6 +162,20 @@ impl NotificationBuilder {
|
|||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
Ok(notification_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_many(
|
||||||
|
&self,
|
||||||
|
users: Vec<DBUserId>,
|
||||||
|
transaction: &mut PgTransaction<'_>,
|
||||||
|
redis: &RedisPool,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
let notification_ids =
|
||||||
|
self.insert_many_records(&users, transaction).await?;
|
||||||
|
|
||||||
|
let users_raw_ids = users.iter().map(|x| x.0).collect::<Vec<_>>();
|
||||||
|
|
||||||
let notification_types = notification_ids
|
let notification_types = notification_ids
|
||||||
.iter()
|
.iter()
|
||||||
.map(|_| self.body.notification_type().as_str())
|
.map(|_| self.body.notification_type().as_str())
|
||||||
@@ -181,6 +194,19 @@ impl NotificationBuilder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [`insert_many`], but skips queuing deliveries so the caller can
|
||||||
|
/// manually send the notifications.
|
||||||
|
pub async fn insert_many_without_delivery(
|
||||||
|
&self,
|
||||||
|
users: Vec<DBUserId>,
|
||||||
|
transaction: &mut PgTransaction<'_>,
|
||||||
|
redis: &RedisPool,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
self.insert_many_records(&users, transaction).await?;
|
||||||
|
DBNotification::clear_user_notifications_cache(&users, redis).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn insert_many_deliveries(
|
pub async fn insert_many_deliveries(
|
||||||
transaction: &mut PgTransaction<'_>,
|
transaction: &mut PgTransaction<'_>,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
|
|||||||
@@ -5,19 +5,28 @@ use crate::database::models::notification_item::NotificationBuilder;
|
|||||||
use crate::database::models::user_item::DBUser;
|
use crate::database::models::user_item::DBUser;
|
||||||
use crate::database::redis::RedisPool;
|
use crate::database::redis::RedisPool;
|
||||||
use crate::models::users::Role;
|
use crate::models::users::Role;
|
||||||
use crate::models::v3::notifications::NotificationBody;
|
use crate::models::v3::notifications::{
|
||||||
|
NotificationBody, NotificationDeliveryStatus,
|
||||||
|
};
|
||||||
use crate::models::v3::pats::Scopes;
|
use crate::models::v3::pats::Scopes;
|
||||||
|
use crate::queue::email::EmailQueue;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::guards::external_notification_key_guard;
|
use crate::util::guards::external_notification_key_guard;
|
||||||
use actix_web::HttpRequest;
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use actix_web::{HttpResponse, post};
|
use actix_web::{
|
||||||
|
CustomizeResponder, HttpRequest, HttpResponse, Responder, post,
|
||||||
|
};
|
||||||
use ariadne::ids::UserId;
|
use ariadne::ids::UserId;
|
||||||
|
use eyre::eyre;
|
||||||
|
use lettre::message::Mailbox;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(create).service(send_custom_email);
|
cfg.service(create)
|
||||||
|
.service(create_direct_email)
|
||||||
|
.service(send_custom_email);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -56,6 +65,99 @@ pub async fn create(
|
|||||||
Ok(HttpResponse::Accepted().finish())
|
Ok(HttpResponse::Accepted().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Directly sends emails to users and inserts notifications when emails are
|
||||||
|
/// delivered successfully.
|
||||||
|
///
|
||||||
|
/// Responds with the user IDs that could not be emailed:
|
||||||
|
/// - `200` if every recipient was delivered (empty list)
|
||||||
|
/// - `207` if some recipients failed (list of failed IDs)
|
||||||
|
/// - `500` if no recipient was delivered
|
||||||
|
#[post(
|
||||||
|
"external_notifications/direct-email",
|
||||||
|
guard = "external_notification_key_guard"
|
||||||
|
)]
|
||||||
|
pub async fn create_direct_email(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
email_queue: web::Data<EmailQueue>,
|
||||||
|
create_notification: web::Json<CreateNotification>,
|
||||||
|
) -> Result<CustomizeResponder<web::Json<Vec<UserId>>>, ApiError> {
|
||||||
|
let CreateNotification { body, user_ids } =
|
||||||
|
create_notification.into_inner();
|
||||||
|
let user_ids = user_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| DBUserId(x.0 as i64))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut txn = pool.begin().await?;
|
||||||
|
|
||||||
|
if !DBUser::exists_many(&user_ids, &mut txn).await? {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"One of the specified users do not exist.".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results: Vec<Result<DBUserId, DBUserId>> =
|
||||||
|
Vec::with_capacity(user_ids.len());
|
||||||
|
|
||||||
|
for user_id in &user_ids {
|
||||||
|
let user = DBUser::get_id(*user_id, &mut txn, &redis).await?.ok_or(
|
||||||
|
ApiError::Internal(eyre!(
|
||||||
|
"user `{}` disappeared while sending notification email",
|
||||||
|
user_id.0
|
||||||
|
)),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let delivered =
|
||||||
|
match user.email.and_then(|email| email.parse::<Mailbox>().ok()) {
|
||||||
|
Some(mailbox) => {
|
||||||
|
email_queue
|
||||||
|
.send_one(&mut txn, body.clone(), *user_id, mailbox)
|
||||||
|
.await?
|
||||||
|
== NotificationDeliveryStatus::Delivered
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
results.push(if delivered {
|
||||||
|
Ok(*user_id)
|
||||||
|
} else {
|
||||||
|
Err(*user_id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let delivered = results
|
||||||
|
.iter()
|
||||||
|
.filter_map(|result| result.as_ref().ok().copied())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if delivered.is_empty() {
|
||||||
|
return Err(ApiError::Internal(eyre!(
|
||||||
|
"failed to deliver notification email to any of {} recipients",
|
||||||
|
user_ids.len(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationBuilder { body }
|
||||||
|
.insert_many_without_delivery(delivered, &mut txn, &redis)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
txn.commit().await?;
|
||||||
|
|
||||||
|
let failed = results
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|result| result.err().map(|id| UserId(id.0 as u64)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let status = if failed.is_empty() {
|
||||||
|
StatusCode::OK
|
||||||
|
} else {
|
||||||
|
StatusCode::MULTI_STATUS
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(web::Json(failed).customize().with_status(status))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SendEmail {
|
struct SendEmail {
|
||||||
pub users: Vec<UserId>,
|
pub users: Vec<UserId>,
|
||||||
|
|||||||
Reference in New Issue
Block a user