use db::sqlx; use model::notify::UserAppNotifyModel; use model::users::UserNotificationModel; use serde::{Deserialize, Serialize}; use session::Session; use crate::{AppService, Pagination, error::AppError, session_user}; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct UserNotificationConfig { pub email_enabled: bool, pub in_app_enabled: bool, pub push_enabled: bool, pub digest_mode: String, pub dnd_enabled: bool, pub dnd_start_minute: Option, pub dnd_end_minute: Option, pub marketing_enabled: bool, pub security_enabled: bool, pub product_enabled: bool, pub push_subscription_endpoint: Option, pub push_subscription_keys_p256dh: Option, pub push_subscription_keys_auth: Option, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct AppNotificationItem { pub id: uuid::Uuid, pub title: String, pub body: String, pub notify_type: String, #[schema(value_type = Option)] pub read_at: Option>, #[schema(value_type = String)] pub created_at: chrono::DateTime, } impl From for AppNotificationItem { fn from(n: UserAppNotifyModel) -> Self { Self { id: n.id, title: n.title, body: n.body, notify_type: n.notify_type, read_at: n.read_at, created_at: n.created_at, } } } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct UpdateUserNotificationConfig { pub email_enabled: Option, pub in_app_enabled: Option, pub push_enabled: Option, pub digest_mode: Option, pub dnd_enabled: Option, pub dnd_start_minute: Option>, pub dnd_end_minute: Option>, pub marketing_enabled: Option, pub security_enabled: Option, pub product_enabled: Option, pub push_subscription_endpoint: Option>, pub push_subscription_keys_p256dh: Option>, pub push_subscription_keys_auth: Option>, } impl AppService { pub async fn user_update_notification_config( &self, ctx: &Session, params: UpdateUserNotificationConfig, ) -> Result { let user_uid = session_user(ctx)?; let mut config = self.user_notification_config(user_uid).await?; if let Some(email_enabled) = params.email_enabled { config.email_enabled = email_enabled; } if let Some(in_app_enabled) = params.in_app_enabled { config.in_app_enabled = in_app_enabled; } if let Some(push_enabled) = params.push_enabled { config.push_enabled = push_enabled; } if let Some(digest_mode) = params.digest_mode { config.digest_mode = digest_mode; } if let Some(dnd_enabled) = params.dnd_enabled { config.dnd_enabled = dnd_enabled; } if let Some(dnd_start_minute) = params.dnd_start_minute { config.dnd_start_minute = dnd_start_minute; } if let Some(dnd_end_minute) = params.dnd_end_minute { config.dnd_end_minute = dnd_end_minute; } if let Some(marketing_enabled) = params.marketing_enabled { config.marketing_enabled = marketing_enabled; } if let Some(security_enabled) = params.security_enabled { config.security_enabled = security_enabled; } if let Some(product_enabled) = params.product_enabled { config.product_enabled = product_enabled; } if let Some(push_subscription_endpoint) = params.push_subscription_endpoint { config.push_subscription_endpoint = push_subscription_endpoint; } if let Some(push_subscription_keys_p256dh) = params.push_subscription_keys_p256dh { config.push_subscription_keys_p256dh = push_subscription_keys_p256dh; } if let Some(push_subscription_keys_auth) = params.push_subscription_keys_auth { config.push_subscription_keys_auth = push_subscription_keys_auth; } let now = chrono::Utc::now(); sqlx::query( "INSERT INTO user_notification \ (\"user\", email_enabled, in_app_enabled, push_enabled, digest_mode, dnd_enabled, dnd_start_minute, dnd_end_minute, \ marketing_enabled, security_enabled, product_enabled, push_subscription_endpoint, push_subscription_keys_p256dh, \ push_subscription_keys_auth, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $15) \ ON CONFLICT (\"user\") DO UPDATE SET \ email_enabled = EXCLUDED.email_enabled, in_app_enabled = EXCLUDED.in_app_enabled, push_enabled = EXCLUDED.push_enabled, \ digest_mode = EXCLUDED.digest_mode, dnd_enabled = EXCLUDED.dnd_enabled, dnd_start_minute = EXCLUDED.dnd_start_minute, \ dnd_end_minute = EXCLUDED.dnd_end_minute, marketing_enabled = EXCLUDED.marketing_enabled, security_enabled = EXCLUDED.security_enabled, \ product_enabled = EXCLUDED.product_enabled, push_subscription_endpoint = EXCLUDED.push_subscription_endpoint, \ push_subscription_keys_p256dh = EXCLUDED.push_subscription_keys_p256dh, push_subscription_keys_auth = EXCLUDED.push_subscription_keys_auth, \ updated_at = EXCLUDED.updated_at", ) .bind(user_uid) .bind(config.email_enabled) .bind(config.in_app_enabled) .bind(config.push_enabled) .bind(&config.digest_mode) .bind(config.dnd_enabled) .bind(config.dnd_start_minute) .bind(config.dnd_end_minute) .bind(config.marketing_enabled) .bind(config.security_enabled) .bind(config.product_enabled) .bind(&config.push_subscription_endpoint) .bind(&config.push_subscription_keys_p256dh) .bind(&config.push_subscription_keys_auth) .bind(now) .execute(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(config) } pub async fn list_notifications( &self, ctx: &Session, pagination: Pagination, ) -> Result, AppError> { let user_uid = session_user(ctx)?; let rows = sqlx::query_as::<_, UserAppNotifyModel>( "SELECT id, \"user\", title, body, notify_type, target_type, target_id, metadata, read_at, archived_at, created_at, updated_at \ FROM user_app_notify WHERE \"user\" = $1 AND archived_at IS NULL \ ORDER BY created_at DESC \ OFFSET $2 LIMIT $3", ) .bind(user_uid) .bind(pagination.offset() as i64) .bind(pagination.limit() as i64) .fetch_all(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(rows.into_iter().map(Into::into).collect()) } pub async fn unread_notifications_count( &self, user_uid: uuid::Uuid, ) -> Result { let row: (Option,) = sqlx::query_as( "SELECT COUNT(*) FROM user_app_notify \ WHERE \"user\" = $1 AND read_at IS NULL AND archived_at IS NULL", ) .bind(user_uid) .fetch_one(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(row.0.unwrap_or(0)) } pub async fn user_notification_config( &self, user_uid: uuid::Uuid, ) -> Result { let row = sqlx::query_as::<_, UserNotificationModel>( "SELECT \"user\", email_enabled, in_app_enabled, push_enabled, digest_mode, dnd_enabled, dnd_start_minute, dnd_end_minute, \ marketing_enabled, security_enabled, product_enabled, push_subscription_endpoint, push_subscription_keys_p256dh, \ push_subscription_keys_auth, created_at, updated_at \ FROM user_notification WHERE \"user\" = $1", ) .bind(user_uid) .fetch_optional(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(row.map(Into::into).unwrap_or_default()) } } impl Default for UserNotificationConfig { fn default() -> Self { Self { email_enabled: true, in_app_enabled: true, push_enabled: false, digest_mode: "daily".to_string(), dnd_enabled: false, dnd_start_minute: None, dnd_end_minute: None, marketing_enabled: false, security_enabled: true, product_enabled: true, push_subscription_endpoint: None, push_subscription_keys_p256dh: None, push_subscription_keys_auth: None, } } } impl From for UserNotificationConfig { fn from(value: UserNotificationModel) -> Self { Self { email_enabled: value.email_enabled, in_app_enabled: value.in_app_enabled, push_enabled: value.push_enabled, digest_mode: value.digest_mode, dnd_enabled: value.dnd_enabled, dnd_start_minute: value.dnd_start_minute, dnd_end_minute: value.dnd_end_minute, marketing_enabled: value.marketing_enabled, security_enabled: value.security_enabled, product_enabled: value.product_enabled, push_subscription_endpoint: value.push_subscription_endpoint, push_subscription_keys_p256dh: value.push_subscription_keys_p256dh, push_subscription_keys_auth: value.push_subscription_keys_auth, } } }