You've already forked AstralRinth
forked from didirus/AstralRinth
New Creator Notifications (#4383)
* Some new notification types * Fix error * Use existing DB models rather than inline queries * Fix template fillout * Fix ModerationThreadMessageReceived * Insert more notifications, fix some formatting * chore: query cache, clippy, fmt * chore: query cache, clippy, fmt * Use outer transactions to insert notifications instead of creating a new one * Join futures
This commit is contained in:
committed by
GitHub
parent
8149618187
commit
6da190ed01
@@ -307,7 +307,7 @@ impl EmailQueue {
|
||||
};
|
||||
|
||||
let message = templates::build_email(
|
||||
&mut **txn,
|
||||
txn,
|
||||
&self.redis,
|
||||
&self.client,
|
||||
user_id,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use super::MailError;
|
||||
use crate::database::models::DBUser;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::models::ids::*;
|
||||
use crate::database::models::notifications_template_item::NotificationTemplate;
|
||||
use crate::database::models::{
|
||||
DBOrganization, DBProject, DBUser, DatabaseError,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::NotificationBody;
|
||||
use crate::routes::ApiError;
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use futures::TryFutureExt;
|
||||
use lettre::Message;
|
||||
use lettre::message::{Mailbox, MultiPart, SinglePart};
|
||||
@@ -38,6 +40,27 @@ const STATUSCHANGE_PROJECT_NAME: &str = "statuschange.project.name";
|
||||
const STATUSCHANGE_OLD_STATUS: &str = "statuschange.old.status";
|
||||
const STATUSCHANGE_NEW_STATUS: &str = "statuschange.new.status";
|
||||
|
||||
const NEWPAT_TOKEN_NAME: &str = "newpat.token_name";
|
||||
|
||||
const PROJECT_ID: &str = "project.id";
|
||||
const PROJECT_NAME: &str = "project.name";
|
||||
const PROJECT_ICON_URL: &str = "project.icon_url";
|
||||
|
||||
const REPORT_ID: &str = "report.id";
|
||||
const REPORT_TITLE: &str = "report.title";
|
||||
const REPORT_DATE: &str = "report.date";
|
||||
const NEWREPORT_ID: &str = "newreport.id";
|
||||
|
||||
const PROJECT_OLD_STATUS: &str = "project.oldstatus";
|
||||
const PROJECT_NEW_STATUS: &str = "project.newstatus";
|
||||
|
||||
const NEWOWNER_TYPE: &str = "new_owner.type";
|
||||
const NEWOWNER_TYPE_CAPITALIZED: &str = "new_owner.type_capitalized";
|
||||
const NEWOWNER_NAME: &str = "new_owner.name";
|
||||
|
||||
const PAYOUTAVAILABLE_AMOUNT: &str = "payout.amount";
|
||||
const PAYOUTAVAILABLE_PERIOD: &str = "payout.period";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MailingIdentity {
|
||||
from_name: String,
|
||||
@@ -59,7 +82,7 @@ impl MailingIdentity {
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn build_email(
|
||||
exec: impl sqlx::PgExecutor<'_>,
|
||||
exec: &mut sqlx::PgTransaction<'_>,
|
||||
redis: &RedisPool,
|
||||
client: &reqwest::Client,
|
||||
user_id: DBUserId,
|
||||
@@ -181,27 +204,177 @@ fn fill_template(
|
||||
}
|
||||
|
||||
async fn collect_template_variables(
|
||||
exec: impl sqlx::PgExecutor<'_>,
|
||||
exec: &mut sqlx::PgTransaction<'_>,
|
||||
redis: &RedisPool,
|
||||
user_id: DBUserId,
|
||||
n: &NotificationBody,
|
||||
) -> Result<HashMap<&'static str, String>, ApiError> {
|
||||
async fn only_select_default_variables(
|
||||
exec: impl sqlx::PgExecutor<'_>,
|
||||
redis: &RedisPool,
|
||||
user_id: DBUserId,
|
||||
) -> Result<HashMap<&'static str, String>, ApiError> {
|
||||
let mut map = HashMap::new();
|
||||
let db_user = DBUser::get_id(user_id, &mut **exec, redis)
|
||||
.await?
|
||||
.ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?;
|
||||
|
||||
let user = DBUser::get_id(user_id, exec, redis)
|
||||
.await?
|
||||
.ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?;
|
||||
|
||||
map.insert(USER_NAME, user.username);
|
||||
Ok(map)
|
||||
}
|
||||
let mut map = HashMap::new();
|
||||
map.insert(USER_NAME, db_user.username);
|
||||
|
||||
match &n {
|
||||
NotificationBody::PatCreated { token_name } => {
|
||||
map.insert(NEWPAT_TOKEN_NAME, token_name.clone());
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ModerationMessageReceived { project_id, .. } => {
|
||||
let result = DBProject::get_id(
|
||||
DBProjectId(project_id.0 as i64),
|
||||
exec,
|
||||
redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?
|
||||
.inner;
|
||||
|
||||
map.insert(PROJECT_ID, to_base62(project_id.0));
|
||||
map.insert(PROJECT_NAME, result.name);
|
||||
map.insert(PROJECT_ICON_URL, result.icon_url.unwrap_or_default());
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ReportStatusUpdated { report_id } => {
|
||||
let result = query!(
|
||||
r#"
|
||||
SELECT
|
||||
r.created,
|
||||
COALESCE(m.name, v.version_number, u.username, 'unknown') "title!"
|
||||
FROM reports r
|
||||
LEFT JOIN mods m ON r.mod_id = m.id
|
||||
LEFT JOIN versions v ON r.version_id = v.id
|
||||
LEFT JOIN users u ON r.user_id = u.id
|
||||
WHERE r.id = $1
|
||||
"#,
|
||||
report_id.0 as i64
|
||||
)
|
||||
.fetch_one(&mut **exec)
|
||||
.await?;
|
||||
|
||||
map.insert(REPORT_ID, to_base62(report_id.0));
|
||||
map.insert(REPORT_TITLE, result.title);
|
||||
map.insert(REPORT_DATE, date_human_readable(result.created));
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ReportSubmitted { report_id } => {
|
||||
let result = query!(
|
||||
r#"
|
||||
SELECT
|
||||
COALESCE(m.name, v.version_number, u.username, 'unknown') "title!"
|
||||
FROM reports r
|
||||
LEFT JOIN mods m ON r.mod_id = m.id
|
||||
LEFT JOIN versions v ON r.version_id = v.id
|
||||
LEFT JOIN users u ON r.user_id = u.id
|
||||
WHERE r.id = $1
|
||||
"#,
|
||||
report_id.0 as i64
|
||||
)
|
||||
.fetch_one(&mut **exec)
|
||||
.await?;
|
||||
|
||||
map.insert(REPORT_TITLE, result.title);
|
||||
map.insert(NEWREPORT_ID, to_base62(report_id.0));
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ProjectStatusApproved { project_id } => {
|
||||
let result = query!(
|
||||
r#"
|
||||
SELECT name, icon_url FROM mods WHERE id = $1
|
||||
"#,
|
||||
project_id.0 as i64
|
||||
)
|
||||
.fetch_one(&mut **exec)
|
||||
.await?;
|
||||
|
||||
map.insert(PROJECT_ID, to_base62(project_id.0));
|
||||
map.insert(PROJECT_NAME, result.name);
|
||||
map.insert(PROJECT_ICON_URL, result.icon_url.unwrap_or_default());
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ProjectStatusNeutral {
|
||||
project_id,
|
||||
old_status,
|
||||
new_status,
|
||||
} => {
|
||||
let result = DBProject::get_id(
|
||||
DBProjectId(project_id.0 as i64),
|
||||
exec,
|
||||
redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?
|
||||
.inner;
|
||||
|
||||
map.insert(PROJECT_ID, to_base62(project_id.0));
|
||||
map.insert(PROJECT_NAME, result.name);
|
||||
map.insert(PROJECT_ICON_URL, result.icon_url.unwrap_or_default());
|
||||
map.insert(PROJECT_OLD_STATUS, old_status.as_str().to_string());
|
||||
map.insert(PROJECT_NEW_STATUS, new_status.as_str().to_string());
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ProjectTransferred {
|
||||
project_id,
|
||||
new_owner_user_id,
|
||||
new_owner_organization_id,
|
||||
} => {
|
||||
let project = DBProject::get_id(
|
||||
DBProjectId(project_id.0 as i64),
|
||||
&mut **exec,
|
||||
redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| DatabaseError::Database(sqlx::Error::RowNotFound))?
|
||||
.inner;
|
||||
|
||||
map.insert(PROJECT_ID, to_base62(project_id.0));
|
||||
map.insert(PROJECT_NAME, project.name);
|
||||
map.insert(PROJECT_ICON_URL, project.icon_url.unwrap_or_default());
|
||||
|
||||
if let Some(new_owner_user_id) = new_owner_user_id {
|
||||
let user = DBUser::get_id(
|
||||
DBUserId(new_owner_user_id.0 as i64),
|
||||
&mut **exec,
|
||||
redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
DatabaseError::Database(sqlx::Error::RowNotFound)
|
||||
})?;
|
||||
|
||||
map.insert(NEWOWNER_TYPE, "user".to_string());
|
||||
map.insert(NEWOWNER_TYPE_CAPITALIZED, "User".to_string());
|
||||
map.insert(NEWOWNER_NAME, user.username);
|
||||
} else if let Some(new_owner_organization_id) =
|
||||
new_owner_organization_id
|
||||
{
|
||||
let org = DBOrganization::get_id(
|
||||
DBOrganizationId(new_owner_organization_id.0 as i64),
|
||||
&mut **exec,
|
||||
redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
DatabaseError::Database(sqlx::Error::RowNotFound)
|
||||
})?;
|
||||
|
||||
map.insert(NEWOWNER_TYPE, "organization".to_string());
|
||||
map.insert(
|
||||
NEWOWNER_TYPE_CAPITALIZED,
|
||||
"Organization".to_string(),
|
||||
);
|
||||
map.insert(NEWOWNER_NAME, org.name);
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
NotificationBody::TeamInvite {
|
||||
team_id: _,
|
||||
project_id,
|
||||
@@ -224,11 +397,9 @@ async fn collect_template_variables(
|
||||
project_id.0 as i64,
|
||||
user_id.0 as i64
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.fetch_one(&mut **exec)
|
||||
.await?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(USER_NAME, result.user_name);
|
||||
map.insert(TEAMINVITE_INVITER_NAME, result.inviter_name);
|
||||
map.insert(TEAMINVITE_PROJECT_NAME, result.project_name);
|
||||
map.insert(TEAMINVITE_ROLE_NAME, role.clone());
|
||||
@@ -258,11 +429,9 @@ async fn collect_template_variables(
|
||||
organization_id.0 as i64,
|
||||
user_id.0 as i64
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.fetch_one(&mut **exec)
|
||||
.await?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(USER_NAME, result.user_name);
|
||||
map.insert(ORGINVITE_INVITER_NAME, result.inviter_name);
|
||||
map.insert(ORGINVITE_ORG_NAME, result.organization_name);
|
||||
map.insert(ORGINVITE_ROLE_NAME, role.clone());
|
||||
@@ -288,11 +457,9 @@ async fn collect_template_variables(
|
||||
project_id.0 as i64,
|
||||
user_id.0 as i64,
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.fetch_one(&mut **exec)
|
||||
.await?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(USER_NAME, result.user_name);
|
||||
map.insert(STATUSCHANGE_PROJECT_NAME, result.project_name);
|
||||
map.insert(STATUSCHANGE_OLD_STATUS, old_status.as_str().to_owned());
|
||||
map.insert(STATUSCHANGE_NEW_STATUS, new_status.as_str().to_owned());
|
||||
@@ -308,13 +475,7 @@ async fn collect_template_variables(
|
||||
flow
|
||||
);
|
||||
|
||||
let user = DBUser::get_id(user_id, exec, redis).await?.ok_or_else(
|
||||
|| DatabaseError::Database(sqlx::Error::RowNotFound),
|
||||
)?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(RESETPASSWORD_URL, url);
|
||||
map.insert(USER_NAME, user.username);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
@@ -327,25 +488,13 @@ async fn collect_template_variables(
|
||||
flow
|
||||
);
|
||||
|
||||
let user = DBUser::get_id(user_id, exec, redis).await?.ok_or_else(
|
||||
|| DatabaseError::Database(sqlx::Error::RowNotFound),
|
||||
)?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(VERIFYEMAIL_URL, url);
|
||||
map.insert(USER_NAME, user.username);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::AuthProviderAdded { provider }
|
||||
| NotificationBody::AuthProviderRemoved { provider } => {
|
||||
let user = DBUser::get_id(user_id, exec, redis).await?.ok_or_else(
|
||||
|| DatabaseError::Database(sqlx::Error::RowNotFound),
|
||||
)?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(USER_NAME, user.username);
|
||||
map.insert(AUTHPROVIDER_NAME, provider.clone());
|
||||
|
||||
Ok(map)
|
||||
@@ -354,30 +503,18 @@ async fn collect_template_variables(
|
||||
NotificationBody::TwoFactorEnabled
|
||||
| NotificationBody::TwoFactorRemoved
|
||||
| NotificationBody::PasswordChanged
|
||||
| NotificationBody::PasswordRemoved => {
|
||||
only_select_default_variables(exec, redis, user_id).await
|
||||
}
|
||||
| NotificationBody::PasswordRemoved => Ok(map),
|
||||
|
||||
NotificationBody::EmailChanged {
|
||||
new_email,
|
||||
to_email: _,
|
||||
} => {
|
||||
let user = DBUser::get_id(user_id, exec, redis).await?.ok_or_else(
|
||||
|| DatabaseError::Database(sqlx::Error::RowNotFound),
|
||||
)?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(USER_NAME, user.username);
|
||||
map.insert(EMAILCHANGED_NEW_EMAIL, new_email.clone());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::PaymentFailed { amount, service } => {
|
||||
let user = DBUser::get_id(user_id, exec, redis).await?.ok_or_else(
|
||||
|| DatabaseError::Database(sqlx::Error::RowNotFound),
|
||||
)?;
|
||||
|
||||
let url = format!(
|
||||
"{}/{}",
|
||||
dotenvy::var("SITE_URL")?,
|
||||
@@ -385,7 +522,6 @@ async fn collect_template_variables(
|
||||
);
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert(USER_NAME, user.username);
|
||||
map.insert(PAYMENTFAILED_AMOUNT, amount.clone());
|
||||
map.insert(PAYMENTFAILED_SERVICE, service.clone());
|
||||
map.insert(BILLING_URL, url);
|
||||
@@ -393,11 +529,34 @@ async fn collect_template_variables(
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ProjectUpdate { .. }
|
||||
| NotificationBody::LegacyMarkdown { .. }
|
||||
| NotificationBody::ModeratorMessage { .. }
|
||||
| NotificationBody::Unknown => {
|
||||
only_select_default_variables(exec, redis, user_id).await
|
||||
NotificationBody::PayoutAvailable {
|
||||
amount,
|
||||
date_available,
|
||||
} => {
|
||||
if let Some(period_month) =
|
||||
date_available.checked_sub_months(chrono::Months::new(2))
|
||||
{
|
||||
map.insert(
|
||||
PAYOUTAVAILABLE_PERIOD,
|
||||
period_month.format("%B %Y").to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
map.insert(
|
||||
PAYOUTAVAILABLE_AMOUNT,
|
||||
format!("{:.2}", (amount * 100.0) as i64),
|
||||
);
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
NotificationBody::ProjectUpdate { .. }
|
||||
| NotificationBody::ModeratorMessage { .. }
|
||||
| NotificationBody::LegacyMarkdown { .. }
|
||||
| NotificationBody::Unknown => Ok(map),
|
||||
}
|
||||
}
|
||||
|
||||
fn date_human_readable(date: chrono::DateTime<chrono::Utc>) -> String {
|
||||
date.format("%B %d, %Y").to_string()
|
||||
}
|
||||
|
||||
@@ -711,7 +711,19 @@ impl AutomatedModerationQueue {
|
||||
},
|
||||
}
|
||||
.insert_many(
|
||||
members.into_iter().map(|x| x.user_id).collect(),
|
||||
members.iter().map(|x| x.user_id).collect(),
|
||||
&mut transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
NotificationBuilder {
|
||||
body: NotificationBody::ModerationMessageReceived {
|
||||
project_id: project.inner.id.into(),
|
||||
},
|
||||
}
|
||||
.insert_many(
|
||||
members.iter().map(|x| x.user_id).collect(),
|
||||
&mut transaction,
|
||||
&redis,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::payouts_values_notifications;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::payouts::{
|
||||
PayoutDecimal, PayoutInterval, PayoutMethod, PayoutMethodFee,
|
||||
PayoutMethodType,
|
||||
@@ -1084,6 +1087,41 @@ pub async fn insert_payouts(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn index_payouts_notifications(
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), ApiError> {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
payouts_values_notifications::synchronize_future_payout_values(
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?;
|
||||
let items = payouts_values_notifications::PayoutsValuesNotification::unnotified_users_with_available_payouts_with_limit(&mut *transaction, 200).await?;
|
||||
|
||||
let payout_ref_ids = items.iter().map(|x| x.id).collect::<Vec<_>>();
|
||||
let dates_available =
|
||||
items.iter().map(|x| x.date_available).collect::<Vec<_>>();
|
||||
let user_ids = items.iter().map(|x| x.user_id).collect::<Vec<_>>();
|
||||
|
||||
NotificationBuilder::insert_many_payout_notifications(
|
||||
user_ids,
|
||||
dates_available,
|
||||
&mut transaction,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
payouts_values_notifications::PayoutsValuesNotification::set_notified_many(
|
||||
&payout_ref_ids,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_bank_balances_and_webhook(
|
||||
payouts: &PayoutsQueue,
|
||||
pool: &PgPool,
|
||||
|
||||
Reference in New Issue
Block a user