You've already forked AstralRinth
forked from didirus/AstralRinth
[DO NOT MERGE] Email notification system (#4338)
* Migration * Fixup db models * Redis * Stuff * Switch PKs to BIGSERIALs, insert to notifications_deliveries when inserting notifications * Queue, templates * Query cache * Fixes, fixtures * Perf, cache template data & HTML bodies * Notification type configuration, ResetPassword notification type * Reset password * Query cache * Clippy + fmt * Traces, fix typo, fix user email in ResetPassword * send_email * Models, db * Remove dead code, adjust notification settings in migration * Clippy fmt * Delete dead code, fixes * Fmt * Update apps/labrinth/src/queue/email.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Remove old fixtures * Unify email retry delay * Fix type * External notifications * Remove `notifications_types_preference_restrictions`, as user notification preferences is out of scope for this PR * Query cache, fmt, clippy * Fix join in get_many_user_exposed_on_site * Remove migration comment * Query cache * Update html body urls * Remove comment * Add paymentfailed.service variable to PaymentFailed notification variant * Fix compile error * Fix deleting notifications * Update apps/labrinth/src/database/models/user_item.rs Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Update apps/labrinth/src/database/models/user_item.rs Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Update Cargo.toml Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Update apps/labrinth/migrations/20250902133943_notification-extension.sql Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Address review comments * Fix compliation * Update apps/labrinth/src/database/models/users_notifications_preferences_item.rs Co-authored-by: Josiah Glosson <soujournme@gmail.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> * Use strfmt to format emails * Configurable Reply-To * Configurable Reply-To * Refactor for email background task * Send some emails inline * Fix account creation email check * Revert "Use strfmt to format emails" This reverts commit e0d6614afe51fa6349918377e953ba294c34ae0b. * Reintroduce fill_template * Set password reset email inline * Process more emails per index * clippy fmt * Query cache --------- Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Josiah Glosson <soujournme@gmail.com>
This commit is contained in:
committed by
GitHub
parent
1491642209
commit
902d749293
@@ -1,6 +1,8 @@
|
||||
use super::ids::*;
|
||||
use crate::database::{models::DatabaseError, redis::RedisPool};
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::notifications::{
|
||||
NotificationBody, NotificationChannel, NotificationDeliveryStatus,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -55,6 +57,10 @@ impl NotificationBuilder {
|
||||
.map(|_| body.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let users_raw_ids = users.iter().map(|x| x.0).collect::<Vec<_>>();
|
||||
let notification_ids =
|
||||
notification_ids.iter().map(|x| x.0).collect::<Vec<_>>();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO notifications (
|
||||
@@ -62,16 +68,97 @@ impl NotificationBuilder {
|
||||
)
|
||||
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::jsonb[])
|
||||
",
|
||||
¬ification_ids
|
||||
.into_iter()
|
||||
.map(|x| x.0)
|
||||
.collect::<Vec<_>>()[..],
|
||||
&users.iter().map(|x| x.0).collect::<Vec<_>>()[..],
|
||||
¬ification_ids[..],
|
||||
&users_raw_ids[..],
|
||||
&bodies[..],
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
let notification_types = notification_ids
|
||||
.iter()
|
||||
.map(|_| self.body.notification_type().as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let notification_channels = NotificationChannel::list()
|
||||
.iter()
|
||||
.map(|x| x.as_str())
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
// Insert required rows into `notifications_deliveries` by channel
|
||||
// and notification type, based on the user's preferences.
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
WITH
|
||||
channels AS (
|
||||
SELECT channel FROM UNNEST($1::varchar[]) AS t(channel)
|
||||
),
|
||||
delivery_candidates AS (
|
||||
SELECT
|
||||
ids.notification_id,
|
||||
ids.user_id,
|
||||
channels.channel,
|
||||
nt.delivery_priority,
|
||||
uprefs.enabled user_enabled,
|
||||
dprefs.enabled default_enabled
|
||||
FROM
|
||||
UNNEST(
|
||||
$2::bigint[],
|
||||
$3::bigint[],
|
||||
$4::varchar[]
|
||||
) AS ids(notification_id, user_id, notification_type)
|
||||
CROSS JOIN channels
|
||||
INNER JOIN
|
||||
notifications_types nt ON nt.name = ids.notification_type
|
||||
LEFT JOIN users_notifications_preferences uprefs
|
||||
ON uprefs.user_id = ids.user_id
|
||||
AND uprefs.channel = channels.channel
|
||||
AND uprefs.notification_type = ids.notification_type
|
||||
LEFT JOIN users_notifications_preferences dprefs
|
||||
ON dprefs.user_id IS NULL
|
||||
AND dprefs.channel = channels.channel
|
||||
AND dprefs.notification_type = ids.notification_type
|
||||
)
|
||||
INSERT INTO notifications_deliveries
|
||||
(notification_id, user_id, channel, delivery_priority, status, next_attempt, attempt_count)
|
||||
SELECT
|
||||
dc.notification_id,
|
||||
dc.user_id,
|
||||
dc.channel,
|
||||
dc.delivery_priority,
|
||||
CASE
|
||||
-- User explicitly enabled
|
||||
WHEN user_enabled = TRUE THEN $5
|
||||
|
||||
-- Is enabled by default, no preference by user
|
||||
WHEN user_enabled IS NULL AND default_enabled = TRUE THEN $5
|
||||
|
||||
-- User explicitly disabled (regardless of default)
|
||||
WHEN user_enabled = FALSE THEN $6
|
||||
|
||||
-- User set no preference, default disabled
|
||||
WHEN user_enabled IS NULL AND default_enabled = FALSE THEN $7
|
||||
|
||||
-- At this point, user set no preference and there is no
|
||||
-- default set, so treat as disabled-by-default.
|
||||
ELSE $7
|
||||
END status,
|
||||
NOW() next_attempt,
|
||||
0 attempt_count
|
||||
FROM
|
||||
delivery_candidates dc
|
||||
"#,
|
||||
¬ification_channels[..] as &[&str],
|
||||
¬ification_ids[..],
|
||||
&users_raw_ids[..],
|
||||
¬ification_types[..] as &[&str],
|
||||
NotificationDeliveryStatus::Pending.as_str(),
|
||||
NotificationDeliveryStatus::SkippedPreferences.as_str(),
|
||||
NotificationDeliveryStatus::SkippedDefault.as_str(),
|
||||
);
|
||||
|
||||
query.execute(&mut **transaction).await?;
|
||||
|
||||
DBNotification::clear_user_notifications_cache(&users, redis).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -96,7 +183,7 @@ impl DBNotification {
|
||||
exec: E,
|
||||
) -> Result<Vec<DBNotification>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let notification_ids_parsed: Vec<i64> =
|
||||
notification_ids.iter().map(|x| x.0).collect();
|
||||
@@ -144,7 +231,60 @@ impl DBNotification {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_many_user<'a, E>(
|
||||
pub async fn get_all_user<'a, E>(
|
||||
user_id: DBUserId,
|
||||
exec: E,
|
||||
) -> Result<Vec<DBNotification>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let db_notifications = sqlx::query!(
|
||||
"
|
||||
SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body,
|
||||
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
|
||||
FROM notifications n
|
||||
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
|
||||
WHERE n.user_id = $1
|
||||
GROUP BY n.id, n.user_id
|
||||
",
|
||||
user_id as DBUserId
|
||||
)
|
||||
.fetch(exec)
|
||||
.map_ok(|row| {
|
||||
let id = DBNotificationId(row.id);
|
||||
|
||||
DBNotification {
|
||||
id,
|
||||
user_id: DBUserId(row.user_id),
|
||||
read: row.read,
|
||||
created: row.created,
|
||||
body: row.body.clone().and_then(|x| serde_json::from_value(x).ok()).unwrap_or_else(|| {
|
||||
if let Some(name) = row.name {
|
||||
NotificationBody::LegacyMarkdown {
|
||||
notification_type: row.notification_type,
|
||||
name,
|
||||
text: row.text.unwrap_or_default(),
|
||||
link: row.link.unwrap_or_default(),
|
||||
actions: serde_json::from_value(
|
||||
row.actions.unwrap_or_default(),
|
||||
)
|
||||
.ok()
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
} else {
|
||||
NotificationBody::Unknown
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
.try_collect::<Vec<DBNotification>>()
|
||||
.await?;
|
||||
|
||||
Ok(db_notifications)
|
||||
}
|
||||
|
||||
/// Returns user notifications that are configured to be exposed on the website.
|
||||
pub async fn get_many_user_exposed_on_site<'a, E>(
|
||||
user_id: DBUserId,
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
@@ -171,8 +311,10 @@ impl DBNotification {
|
||||
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
|
||||
FROM notifications n
|
||||
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
|
||||
INNER JOIN notifications_types nt on nt.name = n.body ->> 'type'
|
||||
WHERE n.user_id = $1
|
||||
GROUP BY n.id, n.user_id;
|
||||
AND nt.expose_in_site_notifications = TRUE
|
||||
GROUP BY n.id, n.user_id
|
||||
",
|
||||
user_id as DBUserId
|
||||
)
|
||||
@@ -274,6 +416,16 @@ impl DBNotification {
|
||||
let notification_ids_parsed: Vec<i64> =
|
||||
notification_ids.iter().map(|x| x.0).collect();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM notifications_deliveries
|
||||
WHERE notification_id = ANY($1)
|
||||
",
|
||||
¬ification_ids_parsed
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM notifications_actions
|
||||
|
||||
Reference in New Issue
Block a user