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:
François-Xavier Talbot
2025-09-17 15:37:21 -04:00
committed by GitHub
parent 8149618187
commit 6da190ed01
25 changed files with 1211 additions and 77 deletions

View File

@@ -60,12 +60,15 @@ macro_rules! generate_bulk_ids {
count: usize,
con: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<$return_type>, DatabaseError> {
let mut rng = rand::thread_rng();
let mut retry_count = 0;
// Check if ID is unique
loop {
let base = random_base62_rng_range(&mut rng, 1, 10) as i64;
// We re-acquire a thread-local RNG handle for each uniqueness loop for
// the bulk generator future to be `Send + Sync`.
let base =
random_base62_rng_range(&mut rand::thread_rng(), 1, 10)
as i64;
let ids =
(0..count).map(|x| base + x as i64).collect::<Vec<_>>();

View File

@@ -19,6 +19,7 @@ pub mod oauth_token_item;
pub mod organization_item;
pub mod pat_item;
pub mod payout_item;
pub mod payouts_values_notifications;
pub mod product_item;
pub mod project_item;
pub mod report_item;

View File

@@ -2,6 +2,7 @@ use super::ids::*;
use crate::database::{models::DatabaseError, redis::RedisPool};
use crate::models::notifications::{
NotificationBody, NotificationChannel, NotificationDeliveryStatus,
NotificationType,
};
use chrono::{DateTime, Utc};
use futures::TryStreamExt;
@@ -41,6 +42,71 @@ impl NotificationBuilder {
self.insert_many(vec![user], transaction, redis).await
}
pub async fn insert_many_payout_notifications(
users: Vec<DBUserId>,
dates_available: Vec<DateTime<Utc>>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let notification_ids =
generate_many_notification_ids(users.len(), &mut *transaction)
.await?;
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!(
"
WITH
period_payouts AS (
SELECT
ids.notification_id,
ids.user_id,
ids.date_available,
COALESCE(SUM(pv.amount), 0.0) sum
FROM UNNEST($1::bigint[], $2::bigint[], $3::timestamptz[]) AS ids(notification_id, user_id, date_available)
LEFT JOIN payouts_values pv ON pv.user_id = ids.user_id AND pv.date_available = ids.date_available
GROUP BY ids.user_id, ids.notification_id, ids.date_available
)
INSERT INTO notifications (
id, user_id, body
)
SELECT
notification_id id,
user_id,
JSONB_BUILD_OBJECT(
'type', 'payout_available',
'date_available', to_jsonb(date_available),
'amount', to_jsonb(sum)
) body
FROM period_payouts
",
&notification_ids[..],
&users_raw_ids[..],
&dates_available[..],
)
.execute(&mut **transaction)
.await?;
let notification_types = notification_ids
.iter()
.map(|_| NotificationType::PayoutAvailable.as_str())
.collect::<Vec<_>>();
NotificationBuilder::insert_many_deliveries(
transaction,
redis,
&notification_ids,
&users_raw_ids,
&notification_types,
&users,
)
.await?;
Ok(())
}
pub async fn insert_many(
&self,
users: Vec<DBUserId>,
@@ -80,6 +146,27 @@ impl NotificationBuilder {
.map(|_| self.body.notification_type().as_str())
.collect::<Vec<_>>();
NotificationBuilder::insert_many_deliveries(
transaction,
redis,
&notification_ids,
&users_raw_ids,
&notification_types,
&users,
)
.await?;
Ok(())
}
async fn insert_many_deliveries(
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
notification_ids: &[i64],
users_raw_ids: &[i64],
notification_types: &[&str],
users: &[DBUserId],
) -> Result<(), DatabaseError> {
let notification_channels = NotificationChannel::list()
.iter()
.map(|x| x.as_str())
@@ -159,7 +246,7 @@ impl NotificationBuilder {
query.execute(&mut **transaction).await?;
DBNotification::clear_user_notifications_cache(&users, redis).await?;
DBNotification::clear_user_notifications_cache(users, redis).await?;
Ok(())
}

View File

@@ -0,0 +1,89 @@
use crate::database::models::{DBUserId, DatabaseError};
use chrono::{DateTime, Utc};
pub struct PayoutsValuesNotification {
pub id: i32,
pub user_id: DBUserId,
pub date_available: DateTime<Utc>,
}
impl PayoutsValuesNotification {
pub async fn unnotified_users_with_available_payouts_with_limit(
exec: impl sqlx::PgExecutor<'_>,
limit: i64,
) -> Result<Vec<PayoutsValuesNotification>, DatabaseError> {
Ok(sqlx::query_as!(
QueryResult,
"
SELECT
id,
user_id,
date_available
FROM payouts_values_notifications
WHERE
notified = FALSE
AND date_available <= NOW()
FOR UPDATE SKIP LOCKED
LIMIT $1
",
limit,
)
.fetch_all(exec)
.await?
.into_iter()
.map(Into::into)
.collect())
}
pub async fn set_notified_many(
ids: &[i32],
exec: impl sqlx::PgExecutor<'_>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
UPDATE payouts_values_notifications
SET notified = TRUE
WHERE id = ANY($1)
",
&ids[..],
)
.execute(exec)
.await?;
Ok(())
}
}
pub async fn synchronize_future_payout_values(
exec: impl sqlx::PgExecutor<'_>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO payouts_values_notifications (date_available, user_id, notified)
SELECT DISTINCT date_available, user_id, false notified
FROM payouts_values
WHERE date_available > NOW()
ON CONFLICT (date_available, user_id) DO NOTHING
",
)
.execute(exec)
.await?;
Ok(())
}
struct QueryResult {
id: i32,
user_id: i64,
date_available: DateTime<Utc>,
}
impl From<QueryResult> for PayoutsValuesNotification {
fn from(result: QueryResult) -> Self {
PayoutsValuesNotification {
id: result.id,
user_id: DBUserId(result.user_id),
date_available: result.date_available,
}
}
}