245 lines
8.8 KiB
Rust
245 lines
8.8 KiB
Rust
use crate::connection::extract_get_redis;
|
|
use crate::error::RoomError;
|
|
use crate::service::RoomService;
|
|
use crate::ws_context::WsUserContext;
|
|
use chrono::Utc;
|
|
use deadpool_redis::redis;
|
|
use models::Expr;
|
|
use models::rooms::room_notifications;
|
|
use models::users::user as user_model;
|
|
use sea_orm::*;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
impl RoomService {
|
|
pub async fn notification_create(
|
|
&self,
|
|
request: super::NotificationCreateRequest,
|
|
) -> Result<super::NotificationResponse, RoomError> {
|
|
let notification_type = match request.notification_type {
|
|
super::NotificationType::Mention => room_notifications::NotificationType::Mention,
|
|
super::NotificationType::Invitation => room_notifications::NotificationType::Invitation,
|
|
super::NotificationType::RoleChange => room_notifications::NotificationType::RoleChange,
|
|
super::NotificationType::RoomCreated => {
|
|
room_notifications::NotificationType::RoomCreated
|
|
}
|
|
super::NotificationType::RoomDeleted => {
|
|
room_notifications::NotificationType::RoomDeleted
|
|
}
|
|
super::NotificationType::SystemAnnouncement => {
|
|
room_notifications::NotificationType::SystemAnnouncement
|
|
}
|
|
super::NotificationType::ProjectInvitation => {
|
|
room_notifications::NotificationType::ProjectInvitation
|
|
}
|
|
};
|
|
|
|
let model = room_notifications::ActiveModel {
|
|
id: Set(Uuid::now_v7()),
|
|
room: Set(request.room_id),
|
|
project: Set(Some(request.project_id)),
|
|
user_id: Set(Some(request.user_id)),
|
|
notification_type: Set(notification_type),
|
|
related_message_id: Set(request.related_message_id),
|
|
related_user_id: Set(request.related_user_id),
|
|
related_room_id: Set(request.related_room_id),
|
|
title: Set(request.title),
|
|
content: Set(request.content),
|
|
metadata: Set(request.metadata),
|
|
is_read: Set(false),
|
|
is_archived: Set(false),
|
|
created_at: Set(Utc::now()),
|
|
read_at: Set(None),
|
|
expires_at: Set(request.expires_at),
|
|
}
|
|
.insert(&self.db)
|
|
.await?;
|
|
|
|
let user_info = user_model::Entity::find()
|
|
.filter(user_model::Column::Uid.eq(request.user_id))
|
|
.one(&self.db)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|u| super::UserInfo {
|
|
uid: u.uid,
|
|
username: u.username,
|
|
avatar_url: u.avatar_url,
|
|
});
|
|
|
|
let response = super::NotificationResponse {
|
|
id: model.id,
|
|
room: model.room,
|
|
project: model.project,
|
|
user_id: model.user_id,
|
|
user_info,
|
|
notification_type: model.notification_type.to_string(),
|
|
title: model.title,
|
|
content: model.content,
|
|
related_message_id: model.related_message_id,
|
|
related_user_id: model.related_user_id,
|
|
related_room_id: model.related_room_id,
|
|
metadata: model.metadata.unwrap_or(serde_json::json!({})),
|
|
is_read: model.is_read,
|
|
is_archived: model.is_archived,
|
|
created_at: model.created_at,
|
|
read_at: model.read_at,
|
|
expires_at: model.expires_at,
|
|
};
|
|
|
|
self.push_notification_event(request.user_id, response.clone())
|
|
.await;
|
|
self.incr_unread_count_cache(request.user_id).await;
|
|
Ok(response)
|
|
}
|
|
|
|
pub async fn notification_mark_read(
|
|
&self,
|
|
notification_id: Uuid,
|
|
ctx: &WsUserContext,
|
|
) -> Result<(), RoomError> {
|
|
let user_id = ctx.user_id;
|
|
let model = room_notifications::Entity::find_by_id(notification_id)
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| RoomError::NotFound("Notification not found".to_string()))?;
|
|
if model.user_id != Some(user_id) {
|
|
return Err(RoomError::NoPower);
|
|
}
|
|
|
|
if !model.is_read {
|
|
let mut active: room_notifications::ActiveModel = model.into();
|
|
active.is_read = Set(true);
|
|
active.read_at = Set(Some(Utc::now()));
|
|
active.update(&self.db).await?;
|
|
self.decr_unread_count_cache(user_id).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn notification_mark_all_read(&self, ctx: &WsUserContext) -> Result<u64, RoomError> {
|
|
let user_id = ctx.user_id;
|
|
let result = room_notifications::Entity::update_many()
|
|
.filter(room_notifications::Column::UserId.eq(user_id))
|
|
.filter(room_notifications::Column::IsArchived.eq(false))
|
|
.filter(room_notifications::Column::IsRead.eq(false))
|
|
.col_expr(room_notifications::Column::IsRead, Expr::value(true))
|
|
.col_expr(
|
|
room_notifications::Column::ReadAt,
|
|
Expr::value(Some(Utc::now())),
|
|
)
|
|
.exec(&self.db)
|
|
.await?;
|
|
self.reset_unread_count_cache(user_id).await;
|
|
Ok(result.rows_affected)
|
|
}
|
|
|
|
pub async fn notification_archive(
|
|
&self,
|
|
notification_id: Uuid,
|
|
ctx: &WsUserContext,
|
|
) -> Result<(), RoomError> {
|
|
let user_id = ctx.user_id;
|
|
let model = room_notifications::Entity::find_by_id(notification_id)
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| RoomError::NotFound("Notification not found".to_string()))?;
|
|
if model.user_id != Some(user_id) {
|
|
return Err(RoomError::NoPower);
|
|
}
|
|
|
|
let mut active: room_notifications::ActiveModel = model.into();
|
|
active.is_archived = Set(true);
|
|
active.update(&self.db).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn notification_cleanup_expired(&self) -> Result<u64, RoomError> {
|
|
let result = room_notifications::Entity::delete_many()
|
|
.filter(room_notifications::Column::ExpiresAt.lt(Utc::now()))
|
|
.exec(&self.db)
|
|
.await?;
|
|
Ok(result.rows_affected)
|
|
}
|
|
|
|
async fn push_notification_event(
|
|
&self,
|
|
user_id: Uuid,
|
|
notification: super::NotificationResponse,
|
|
) {
|
|
let deep_link = build_deep_link(¬ification);
|
|
let deep_link_url = deep_link.clone();
|
|
let event = super::NotificationEvent::new(notification.clone()).with_deep_link(deep_link);
|
|
self.room_manager
|
|
.push_user_notification(user_id, Arc::new(event))
|
|
.await;
|
|
|
|
if let Some(push_fn) = &self.push_fn {
|
|
push_fn(
|
|
user_id,
|
|
notification.title.clone(),
|
|
notification.content.clone(),
|
|
Some(deep_link_url),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn unread_cache_key(user_id: Uuid) -> String {
|
|
format!("room:notification:unread:{}", user_id)
|
|
}
|
|
|
|
async fn incr_unread_count_cache(&self, user_id: Uuid) {
|
|
let get_redis = extract_get_redis(self.queue.clone());
|
|
let key = Self::unread_cache_key(user_id);
|
|
tokio::spawn(async move {
|
|
let Ok(mut conn) = (get_redis)().await else {
|
|
return;
|
|
};
|
|
let _: Result<i64, _> = redis::cmd("INCR").arg(&key).query_async(&mut conn).await;
|
|
let _: Result<(), _> = redis::cmd("EXPIRE")
|
|
.arg(&key)
|
|
.arg(3600)
|
|
.query_async(&mut conn)
|
|
.await;
|
|
});
|
|
}
|
|
|
|
async fn decr_unread_count_cache(&self, user_id: Uuid) {
|
|
let get_redis = extract_get_redis(self.queue.clone());
|
|
let key = Self::unread_cache_key(user_id);
|
|
tokio::spawn(async move {
|
|
let Ok(mut conn) = (get_redis)().await else {
|
|
return;
|
|
};
|
|
let _: Result<(), _> = redis::cmd("EVAL").arg(r#"local c = redis.call('GET', KEYS[1]); if c and tonumber(c) > 0 then return redis.call('DECR', KEYS[1]) else return 0 end"#).arg(1).arg(&key).query_async(&mut conn).await;
|
|
});
|
|
}
|
|
|
|
async fn reset_unread_count_cache(&self, user_id: Uuid) {
|
|
let get_redis = extract_get_redis(self.queue.clone());
|
|
let key = Self::unread_cache_key(user_id);
|
|
tokio::spawn(async move {
|
|
let Ok(mut conn) = (get_redis)().await else {
|
|
return;
|
|
};
|
|
let _: Result<(), _> = redis::cmd("DEL").arg(&key).query_async(&mut conn).await;
|
|
});
|
|
}
|
|
}
|
|
|
|
fn build_deep_link(n: &super::NotificationResponse) -> String {
|
|
if let Some(room_id) = n.related_room_id {
|
|
return format!("/rooms/{}", room_id);
|
|
}
|
|
if let Some(room_id) = n.room {
|
|
return format!("/rooms/{}", room_id);
|
|
}
|
|
if n.related_message_id.is_some() {
|
|
return "/notify".to_string();
|
|
}
|
|
if let Some(project_id) = n.project {
|
|
return format!("/project/{}", project_id);
|
|
}
|
|
"/notify".to_string()
|
|
}
|