Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
554 lines
20 KiB
Go
554 lines
20 KiB
Go
package v1
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/service"
|
|
"github.com/gochat/gochat/internal/ws"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"github.com/gochat/gochat/pkg/pagination"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// NotificationHandler handles Notification CRUD.
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts/notifications_controller.rb
|
|
type NotificationHandler struct {
|
|
notificationService *service.NotificationService
|
|
eventPublisher *ws.EventPublisher
|
|
}
|
|
|
|
// NewNotificationHandler creates a new Notification handler with injected service.
|
|
func NewNotificationHandler(notificationService *service.NotificationService) *NotificationHandler {
|
|
return &NotificationHandler{notificationService: notificationService}
|
|
}
|
|
|
|
func (h *NotificationHandler) WithEventPublisher(publisher *ws.EventPublisher) *NotificationHandler {
|
|
h.eventPublisher = publisher
|
|
return h
|
|
}
|
|
|
|
// List returns all notifications for the current user in an account.
|
|
// GET /api/v1/accounts/:account_id/notifications
|
|
// Reference: Chatwoot index — NotificationFinder with pagination
|
|
func (h *NotificationHandler) List(c *gin.Context) {
|
|
if h.notificationService == nil {
|
|
response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrInternal, "Notification service not available")
|
|
return
|
|
}
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
p := pagination.Parse(c)
|
|
if p.PerPage == pagination.DefaultPerPage && c.Query("per_page") == "" {
|
|
p.PerPage = 15
|
|
}
|
|
|
|
result, err := h.notificationService.ListNotificationsByAccountWithOptions(c.Request.Context(), userID, accountID, p.Page, p.PerPage, service.NotificationListOptions{
|
|
IncludeRead: notificationIncludes(c, "read"),
|
|
IncludeSnoozed: notificationIncludes(c, "snoozed"),
|
|
SortOrder: c.DefaultQuery("sort_order", "desc"),
|
|
})
|
|
if err != nil {
|
|
applogger.L().Errorf("List notifications: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to fetch notifications")
|
|
return
|
|
}
|
|
|
|
payload := make([]gin.H, 0, len(result.Notifications))
|
|
for i := range result.Notifications {
|
|
payload = append(payload, h.serializeNotification(c.Request.Context(), &result.Notifications[i]))
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"data": gin.H{
|
|
"meta": gin.H{
|
|
"unread_count": result.UnreadCount,
|
|
"count": result.Total,
|
|
"current_page": p.Page,
|
|
},
|
|
"payload": payload,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Get returns a single notification by ID.
|
|
// GET /api/v1/accounts/:account_id/notifications/:id
|
|
func (h *NotificationHandler) Get(c *gin.Context) {
|
|
if h.notificationService == nil {
|
|
response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrInternal, "Notification service not available")
|
|
return
|
|
}
|
|
notificationID, err := parseUintAnyParam(c, "notification_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id")
|
|
return
|
|
}
|
|
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
notification, svcErr := h.notificationService.GetNotificationByAccount(c.Request.Context(), notificationID, userID, accountID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
|
|
}
|
|
|
|
// Update marks a notification as read.
|
|
// PUT /api/v1/accounts/:account_id/notifications/:id
|
|
// Reference: Chatwoot update — @notification.update(read_at: DateTime.now.utc); render json: @notification
|
|
func (h *NotificationHandler) Update(c *gin.Context) {
|
|
notificationID, err := parseUintAnyParam(c, "notification_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id")
|
|
return
|
|
}
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
|
|
notification, svcErr := h.notificationService.MarkReadByAccount(c.Request.Context(), notificationID, userID, accountID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
h.publishNotificationEvent(c.Request.Context(), accountID, userID, ws.EventNotificationUpdated, notification)
|
|
|
|
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
|
|
}
|
|
|
|
// MarkAllRead marks all unread notifications as read for the current user in account.
|
|
// POST /api/v1/accounts/:account_id/notifications/read_all
|
|
// Reference: Chatwoot read_all — update_all(read_at: ...); head :ok
|
|
func (h *NotificationHandler) MarkAllRead(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
var req struct {
|
|
PrimaryActorType string `json:"primary_actor_type"`
|
|
PrimaryActorID uint `json:"primary_actor_id"`
|
|
}
|
|
_ = bindOptionalNotificationJSON(c, &req)
|
|
|
|
var err error
|
|
if req.PrimaryActorType != "" && req.PrimaryActorID > 0 {
|
|
err = h.notificationService.MarkPrimaryActorReadByAccount(c.Request.Context(), userID, accountID, req.PrimaryActorType, req.PrimaryActorID)
|
|
} else {
|
|
err = h.notificationService.MarkAllReadByAccount(c.Request.Context(), userID, accountID)
|
|
}
|
|
if err != nil {
|
|
applogger.L().Errorf("MarkAllRead notifications: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to mark notifications as read")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// UnreadCount returns the unread notification count.
|
|
// GET /api/v1/accounts/:account_id/notifications/unread_count
|
|
// Reference: Chatwoot unread_count — render json: @unread_count
|
|
func (h *NotificationHandler) UnreadCount(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
|
|
count, err := h.notificationService.GetUnreadCountByAccount(c.Request.Context(), userID, accountID)
|
|
if err != nil {
|
|
applogger.L().Errorf("UnreadCount notifications: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to count unread notifications")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, count)
|
|
}
|
|
|
|
// Snooze snoozes a notification until a specified time.
|
|
// POST /api/v1/accounts/:account_id/notifications/:id/snooze
|
|
// Reference: Chatwoot snooze — update(snoozed_until: ..., meta: ...)
|
|
func (h *NotificationHandler) Snooze(c *gin.Context) {
|
|
notificationID, err := parseUintAnyParam(c, "notification_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id")
|
|
return
|
|
}
|
|
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
|
|
var req struct {
|
|
SnoozedUntil interface{} `json:"snoozed_until"`
|
|
}
|
|
if err := bindOptionalNotificationJSON(c, &req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
if req.SnoozedUntil == nil || fmt.Sprint(req.SnoozedUntil) == "" {
|
|
notification, svcErr := h.notificationService.GetNotificationByAccount(c.Request.Context(), notificationID, userID, accountID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
|
|
return
|
|
}
|
|
|
|
snoozedUntil, parseErr := parseNotificationUnixTime(req.SnoozedUntil)
|
|
if parseErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid snoozed_until format")
|
|
return
|
|
}
|
|
|
|
notification, svcErr := h.notificationService.SnoozeNotification(c.Request.Context(), notificationID, userID, accountID, snoozedUntil)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
h.publishNotificationEvent(c.Request.Context(), accountID, userID, ws.EventNotificationUpdated, notification)
|
|
|
|
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
|
|
}
|
|
|
|
// Unread marks a notification as unread.
|
|
// POST /api/v1/accounts/:account_id/notifications/:id/unread
|
|
// Reference: Chatwoot unread — @notification.update(read_at: nil); render json: @notification
|
|
func (h *NotificationHandler) Unread(c *gin.Context) {
|
|
notificationID, err := parseUintAnyParam(c, "notification_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id")
|
|
return
|
|
}
|
|
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
|
|
notification, svcErr := h.notificationService.MarkNotificationUnread(c.Request.Context(), notificationID, userID, accountID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
h.publishNotificationEvent(c.Request.Context(), accountID, userID, ws.EventNotificationUpdated, notification)
|
|
|
|
c.JSON(http.StatusOK, h.serializeNotification(c.Request.Context(), notification))
|
|
}
|
|
|
|
// Destroy deletes a single notification.
|
|
// DELETE /api/v1/accounts/:account_id/notifications/:id
|
|
// Reference: Chatwoot destroy — @notification.destroy; head :ok
|
|
func (h *NotificationHandler) Destroy(c *gin.Context) {
|
|
notificationID, err := parseUintAnyParam(c, "notification_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id")
|
|
return
|
|
}
|
|
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
notification, svcErr := h.notificationService.GetNotificationByAccount(c.Request.Context(), notificationID, userID, accountID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
if svcErr := h.notificationService.DeleteNotificationByAccount(c.Request.Context(), notificationID, userID, accountID); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
h.publishNotificationEvent(c.Request.Context(), accountID, userID, ws.EventNotificationDeleted, notification)
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// DestroyAll deletes all (or all read) notifications.
|
|
// DELETE /api/v1/accounts/:account_id/notifications/destroy_all
|
|
// Reference: Chatwoot destroy_all — DeleteNotificationJob; head :ok
|
|
// Query params: type=read → only read; otherwise all
|
|
// Note: Chatwoot uses async DeleteNotificationJob; GoChat currently does synchronous deletion
|
|
func (h *NotificationHandler) DestroyAll(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
userID := getUserID(c)
|
|
deleteType := c.Query("type")
|
|
var req struct {
|
|
Type string `json:"type"`
|
|
}
|
|
if err := bindOptionalNotificationJSON(c, &req); err == nil && req.Type != "" {
|
|
deleteType = req.Type
|
|
}
|
|
|
|
var err error
|
|
if deleteType == "read" {
|
|
err = h.notificationService.DeleteReadNotifications(c.Request.Context(), userID, accountID)
|
|
} else {
|
|
err = h.notificationService.DeleteAllNotifications(c.Request.Context(), userID, accountID)
|
|
}
|
|
if err != nil {
|
|
applogger.L().Errorf("DestroyAll notifications: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to delete notifications")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
func notificationIncludes(c *gin.Context, value string) bool {
|
|
for _, raw := range append(c.QueryArray("includes[]"), c.QueryArray("includes")...) {
|
|
for _, part := range strings.Split(raw, ",") {
|
|
if strings.TrimSpace(part) == value {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (h *NotificationHandler) publishNotificationEvent(ctx context.Context, accountID, userID uint, eventType string, notification *model.Notification) {
|
|
if h.eventPublisher == nil || notification == nil || accountID == 0 || userID == 0 {
|
|
return
|
|
}
|
|
unreadCount, err := h.notificationService.GetUnreadCountByAccount(ctx, userID, accountID)
|
|
if err != nil {
|
|
applogger.L().Warnf("notification event %s unread count: %v", eventType, err)
|
|
return
|
|
}
|
|
total, err := h.notificationService.CountNotificationsByAccount(ctx, userID, accountID)
|
|
if err != nil {
|
|
applogger.L().Warnf("notification event %s total count: %v", eventType, err)
|
|
return
|
|
}
|
|
h.eventPublisher.PublishEvent(accountID, eventType, gin.H{
|
|
"notification": h.serializeNotification(ctx, notification),
|
|
"unread_count": unreadCount,
|
|
"count": total,
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
return gin.H{
|
|
"id": notification.ID,
|
|
"notification_type": notification.NotificationType,
|
|
"push_message_title": notificationTitle(notification),
|
|
"push_message_body": notificationBody(notification),
|
|
"primary_actor_type": notification.PrimaryActorType,
|
|
"primary_actor_id": notification.PrimaryActorID,
|
|
"primary_actor": notificationActor(ctx, db, notification.PrimaryActorType, notification.PrimaryActorID, notification.AccountID),
|
|
"read_at": notification.ReadAt,
|
|
"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,
|
|
"meta": meta,
|
|
"additional_attributes": meta,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
return notification.NotificationType
|
|
}
|
|
|
|
func notificationBody(notification *model.Notification) string {
|
|
return stringFromNotificationAttrs(notification.AdditionalAttributes, "push_message_body", "body", "message")
|
|
}
|
|
|
|
func stringFromNotificationAttrs(raw json.RawMessage, keys ...string) string {
|
|
if len(raw) == 0 {
|
|
return ""
|
|
}
|
|
var attrs map[string]interface{}
|
|
if err := json.Unmarshal(raw, &attrs); err != nil {
|
|
return ""
|
|
}
|
|
for _, key := range keys {
|
|
if value, ok := attrs[key].(string); ok {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func bindOptionalNotificationJSON(c *gin.Context, target interface{}) error {
|
|
if c.Request.Body == nil || c.Request.ContentLength == 0 {
|
|
return nil
|
|
}
|
|
return c.ShouldBindJSON(target)
|
|
}
|
|
|
|
func parseNotificationUnixTime(value interface{}) (time.Time, error) {
|
|
switch v := value.(type) {
|
|
case float64:
|
|
return time.Unix(int64(v), 0).UTC(), nil
|
|
case int64:
|
|
return time.Unix(v, 0).UTC(), nil
|
|
case int:
|
|
return time.Unix(int64(v), 0).UTC(), nil
|
|
case string:
|
|
seconds, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
return time.Unix(seconds, 0).UTC(), nil
|
|
default:
|
|
seconds, err := strconv.ParseInt(fmt.Sprint(v), 10, 64)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
return time.Unix(seconds, 0).UTC(), nil
|
|
}
|
|
}
|