This commit is contained in:
2026-06-10 07:24:13 +08:00
parent ac7f351308
commit 2bb54b21a5
47 changed files with 2912 additions and 305 deletions
+149 -10
View File
@@ -1,6 +1,7 @@
package v1
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -9,6 +10,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
@@ -56,7 +58,7 @@ func (h *NotificationHandler) List(c *gin.Context) {
payload := make([]gin.H, 0, len(result.Notifications))
for i := range result.Notifications {
payload = append(payload, serializeNotification(&result.Notifications[i]))
payload = append(payload, h.serializeNotification(c.Request.Context(), &result.Notifications[i]))
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
@@ -91,7 +93,7 @@ func (h *NotificationHandler) Get(c *gin.Context) {
return
}
c.JSON(http.StatusOK, serializeNotification(notification))
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
}
// Update marks a notification as read.
@@ -112,7 +114,7 @@ func (h *NotificationHandler) Update(c *gin.Context) {
return
}
c.JSON(http.StatusOK, serializeNotification(notification))
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
}
// MarkAllRead marks all unread notifications as read for the current user in account.
@@ -185,7 +187,7 @@ func (h *NotificationHandler) Snooze(c *gin.Context) {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeNotification(notification))
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
return
}
@@ -201,7 +203,7 @@ func (h *NotificationHandler) Snooze(c *gin.Context) {
return
}
c.JSON(http.StatusOK, serializeNotification(notification))
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
}
// Unread marks a notification as unread.
@@ -223,7 +225,7 @@ func (h *NotificationHandler) Unread(c *gin.Context) {
return
}
c.JSON(http.StatusOK, serializeNotification(notification))
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
}
// Destroy deletes a single notification.
@@ -288,7 +290,19 @@ func notificationIncludes(c *gin.Context, value string) bool {
return false
}
func (h *NotificationHandler) serializeNotification(ctx context.Context, notification *model.Notification) gin.H {
var db *gorm.DB
if h != nil && h.notificationService != nil {
db = h.notificationService.DB()
}
return serializeNotificationWithDB(ctx, db, notification)
}
func serializeNotification(notification *model.Notification) gin.H {
return serializeNotificationWithDB(context.Background(), nil, notification)
}
func serializeNotificationWithDB(ctx context.Context, db *gorm.DB, notification *model.Notification) gin.H {
meta := gin.H{}
if len(notification.AdditionalAttributes) > 0 {
_ = json.Unmarshal(notification.AdditionalAttributes, &meta)
@@ -300,10 +314,10 @@ func serializeNotification(notification *model.Notification) gin.H {
"push_message_body": notificationBody(notification),
"primary_actor_type": notification.PrimaryActorType,
"primary_actor_id": notification.PrimaryActorID,
"primary_actor": notificationActor(notification.PrimaryActorType, notification.PrimaryActorID),
"primary_actor": notificationActor(ctx, db, notification.PrimaryActorType, notification.PrimaryActorID, notification.AccountID),
"read_at": notification.ReadAt,
"secondary_actor": notificationActor(notification.SecondaryActorType, notification.SecondaryActorID),
"user": gin.H{"id": notification.UserID},
"secondary_actor": notificationActor(ctx, db, notification.SecondaryActorType, notification.SecondaryActorID, notification.AccountID),
"user": notificationUser(ctx, db, notification.UserID, notification.AccountID),
"created_at": notification.CreatedAt.Unix(),
"last_activity_at": notification.UpdatedAt.Unix(),
"snoozed_until": notification.SnoozedUntil,
@@ -312,13 +326,138 @@ func serializeNotification(notification *model.Notification) gin.H {
}
}
func notificationActor(actorType string, actorID uint) interface{} {
func notificationActor(ctx context.Context, db *gorm.DB, actorType string, actorID uint, accountID *uint) interface{} {
if actorType == "" || actorID == 0 {
return nil
}
if db == nil {
return gin.H{"id": actorID, "type": actorType, "meta": gin.H{}}
}
switch strings.ToLower(actorType) {
case "conversation":
var conversation model.Conversation
query := db.WithContext(ctx).Preload("Contact").Preload("Inbox").First(&conversation, actorID)
if query.Error == nil && notificationAccountMatches(accountID, conversation.AccountID) {
return serializeConversationForNotification(ctx, db, &conversation)
}
case "contact":
var contact model.Contact
query := db.WithContext(ctx).First(&contact, actorID)
if query.Error == nil && notificationAccountMatches(accountID, contact.AccountID) {
return serializeContactForNotification(&contact)
}
case "user":
var user model.User
query := db.WithContext(ctx).First(&user, actorID)
if query.Error == nil && notificationAccountMatches(accountID, user.AccountID) {
return serializeUserForNotification(&user)
}
case "agentbot", "agent_bot":
var bot model.AgentBot
query := db.WithContext(ctx).First(&bot, actorID)
if query.Error == nil && (bot.AccountID == nil || accountID == nil || *bot.AccountID == *accountID) {
return serializeAgentBotSender(&bot)
}
}
return gin.H{"id": actorID, "type": actorType, "meta": gin.H{}}
}
func notificationUser(ctx context.Context, db *gorm.DB, userID uint, accountID *uint) interface{} {
if db == nil || userID == 0 {
return gin.H{"id": userID}
}
var user model.User
query := db.WithContext(ctx).First(&user, userID)
if query.Error == nil && notificationAccountMatches(accountID, user.AccountID) {
return serializeUserForNotification(&user)
}
return gin.H{"id": userID}
}
func notificationAccountMatches(accountID *uint, recordAccountID uint) bool {
return accountID == nil || *accountID == recordAccountID
}
func serializeConversationForNotification(ctx context.Context, db *gorm.DB, conversation *model.Conversation) map[string]any {
payload := map[string]any{
"additional_attributes": jsonObject(conversation.AdditionalAttributes),
"can_reply": conversationCanReply(ctx, db, conversation),
"channel": conversation.ChannelType,
"contact_inbox": nil,
"id": conversationDisplayID(conversation),
"inbox_id": conversation.InboxID,
"labels": labelList(conversation.Labels),
"meta": serializeConversationMeta(ctx, db, conversation),
"status": conversation.Status,
"custom_attributes": jsonObject(conversation.CustomAttributes),
"snoozed_until": conversation.SnoozedUntil,
"unread_count": unreadCount(ctx, db, conversation),
"first_reply_created_at": int64Value(conversation.FirstReplyCreatedAt),
"priority": conversation.Priority,
"waiting_since": int64Value(conversation.WaitingSince),
"agent_last_seen_at": int64Value(conversation.AgentLastSeenAt),
"contact_last_seen_at": int64Value(conversation.ContactLastSeenAt),
"last_activity_at": int64Value(conversation.LastActivityAt),
"timestamp": int64Value(conversation.LastActivityAt),
"created_at": conversation.CreatedAt.Unix(),
"updated_at": float64(conversation.UpdatedAt.UnixNano()) / float64(time.Second),
}
payload["contact_inbox"] = serializeConversationContactInboxForNotification(ctx, db, conversation)
if message := latestChatMessageForNotification(ctx, db, conversation); message != nil {
payload["messages"] = []any{serializeMessage(ctx, db, message, conversation)}
} else {
payload["messages"] = []any{}
}
return payload
}
func serializeConversationContactInboxForNotification(ctx context.Context, db *gorm.DB, conversation *model.Conversation) map[string]any {
if db == nil || conversation.ContactInboxID == nil || *conversation.ContactInboxID == 0 {
return nil
}
var contactInbox model.ContactInbox
if err := db.WithContext(ctx).First(&contactInbox, *conversation.ContactInboxID).Error; err != nil {
return nil
}
return serializeContactInboxShell(ctx, db, &contactInbox)
}
func latestChatMessageForNotification(ctx context.Context, db *gorm.DB, conversation *model.Conversation) *model.Message {
if db == nil || conversation == nil {
return nil
}
var message model.Message
query := db.WithContext(ctx).
Where("conversation_id = ? AND account_id = ? AND message_type != ?", conversation.ID, conversation.AccountID, "activity").
Order("created_at DESC, id DESC").
First(&message)
if query.Error != nil {
return nil
}
return &message
}
func serializeContactForNotification(contact *model.Contact) map[string]any {
payload := serializeContact(contact)
payload["type"] = "contact"
delete(payload, "availability_status")
delete(payload, "last_activity_at")
delete(payload, "created_at")
return payload
}
func serializeUserForNotification(user *model.User) map[string]any {
return map[string]any{
"id": user.ID,
"name": user.Name,
"available_name": nonEmpty(user.DisplayName, user.Name),
"avatar_url": user.AvatarURL,
"type": "user",
"availability_status": availabilityStatus(user.Available),
"thumbnail": user.AvatarURL,
}
}
func notificationTitle(notification *model.Notification) string {
if title := stringFromNotificationAttrs(notification.AdditionalAttributes, "push_message_title", "title"); title != "" {
return title