- N1.1: POST /notifications/push/subscribe, PushService, migration 090 - N1.2: Send Web Push on follow/like/comment/message via CreateNotification - N1.3: GET/PUT /notifications/preferences, migration 093 - Shared NotificationService with PushService for profile, track, comment handlers - Fix MockSocialService GetGlobalFeed, GetTrendingHashtags for tests
268 lines
7.6 KiB
Go
268 lines
7.6 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"veza-backend-api/internal/database"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// NotificationService handles notification operations
|
|
type NotificationService struct {
|
|
db *database.Database
|
|
logger *zap.Logger
|
|
pushService *PushService // optional, for N1.2 Web Push
|
|
}
|
|
|
|
// Notification represents a notification
|
|
type Notification struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
|
Type string `json:"type" db:"type"`
|
|
Title string `json:"title" db:"title"`
|
|
Content string `json:"content" db:"content"`
|
|
Link string `json:"link" db:"link"`
|
|
Read bool `json:"read" db:"read"`
|
|
CreatedAt string `json:"created_at" db:"created_at"`
|
|
}
|
|
|
|
// NewNotificationService creates a new notification service
|
|
func NewNotificationService(db *database.Database, logger *zap.Logger) *NotificationService {
|
|
return &NotificationService{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// SetPushService injects the push service for Web Push (N1.2)
|
|
func (ns *NotificationService) SetPushService(ps *PushService) {
|
|
ns.pushService = ps
|
|
}
|
|
|
|
// CreateNotification creates a new notification and optionally sends Web Push (N1.2)
|
|
func (ns *NotificationService) CreateNotification(userID uuid.UUID, notificationType, title, content, link string) error {
|
|
ctx := context.Background()
|
|
|
|
_, err := ns.db.ExecContext(ctx, `
|
|
INSERT INTO notifications (user_id, type, title, content, link)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
`, userID, notificationType, title, content, link)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create notification: %w", err)
|
|
}
|
|
|
|
// N1.2: Send Web Push if enabled and user has subscriptions
|
|
if ns.pushService != nil {
|
|
prefs, err := ns.GetPreferences(userID)
|
|
if err != nil {
|
|
ns.logger.Warn("failed to get push preferences", zap.Error(err))
|
|
return nil
|
|
}
|
|
var shouldPush bool
|
|
switch notificationType {
|
|
case "follow":
|
|
shouldPush = prefs.PushFollow
|
|
case "like":
|
|
shouldPush = prefs.PushLike
|
|
case "comment":
|
|
shouldPush = prefs.PushComment
|
|
case "new_message", "message":
|
|
shouldPush = prefs.PushMessage
|
|
case "user_mentioned", "mention":
|
|
shouldPush = prefs.PushMention
|
|
default:
|
|
shouldPush = false
|
|
}
|
|
if shouldPush {
|
|
if err := ns.pushService.SendPushToUser(ctx, userID, title, content, link); err != nil {
|
|
ns.logger.Warn("failed to send push notification", zap.Error(err), zap.String("user_id", userID.String()))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetNotifications retrieves notifications for a user
|
|
func (ns *NotificationService) GetNotifications(userID uuid.UUID, unreadOnly bool) ([]Notification, error) {
|
|
ctx := context.Background()
|
|
|
|
query := `
|
|
SELECT id, user_id, type, title, content, link, read, created_at
|
|
FROM notifications
|
|
WHERE user_id = $1
|
|
`
|
|
args := []interface{}{userID}
|
|
|
|
if unreadOnly {
|
|
query += " AND read = FALSE"
|
|
}
|
|
|
|
query += " ORDER BY created_at DESC LIMIT 50"
|
|
|
|
rows, err := ns.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get notifications: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var notifications []Notification
|
|
for rows.Next() {
|
|
var notification Notification
|
|
if err := rows.Scan(
|
|
¬ification.ID,
|
|
¬ification.UserID,
|
|
¬ification.Type,
|
|
¬ification.Title,
|
|
¬ification.Content,
|
|
¬ification.Link,
|
|
¬ification.Read,
|
|
¬ification.CreatedAt,
|
|
); err != nil {
|
|
continue
|
|
}
|
|
notifications = append(notifications, notification)
|
|
}
|
|
|
|
return notifications, nil
|
|
}
|
|
|
|
// MarkAsRead marks a notification as read
|
|
func (ns *NotificationService) MarkAsRead(userID uuid.UUID, notificationID uuid.UUID) error {
|
|
ctx := context.Background()
|
|
|
|
_, err := ns.db.ExecContext(ctx, `
|
|
UPDATE notifications
|
|
SET read = TRUE
|
|
WHERE id = $1 AND user_id = $2
|
|
`, notificationID, userID)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to mark notification as read: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarkAllAsRead marks all notifications as read for a user
|
|
func (ns *NotificationService) MarkAllAsRead(userID uuid.UUID) error {
|
|
ctx := context.Background()
|
|
|
|
_, err := ns.db.ExecContext(ctx, `
|
|
UPDATE notifications
|
|
SET read = TRUE
|
|
WHERE user_id = $1 AND read = FALSE
|
|
`, userID)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to mark all notifications as read: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUnreadCount returns the count of unread notifications
|
|
func (ns *NotificationService) GetUnreadCount(userID uuid.UUID) (int, error) {
|
|
ctx := context.Background()
|
|
|
|
var count int
|
|
err := ns.db.QueryRowContext(ctx, `
|
|
SELECT COUNT(*)
|
|
FROM notifications
|
|
WHERE user_id = $1 AND read = FALSE
|
|
`, userID).Scan(&count)
|
|
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get unread count: %w", err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// DeleteNotification deletes a notification
|
|
func (ns *NotificationService) DeleteNotification(userID uuid.UUID, notificationID uuid.UUID) error {
|
|
ctx := context.Background()
|
|
|
|
_, err := ns.db.ExecContext(ctx, `
|
|
DELETE FROM notifications
|
|
WHERE id = $1 AND user_id = $2
|
|
`, notificationID, userID)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete notification: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteAllNotifications deletes all notifications for a user
|
|
func (ns *NotificationService) DeleteAllNotifications(userID uuid.UUID) error {
|
|
ctx := context.Background()
|
|
|
|
_, err := ns.db.ExecContext(ctx, `
|
|
DELETE FROM notifications
|
|
WHERE user_id = $1
|
|
`, userID)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete all notifications: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NotificationPrefs represents notification preferences (N1.3)
|
|
type NotificationPrefs struct {
|
|
PushFollow bool `json:"push_follow"`
|
|
PushLike bool `json:"push_like"`
|
|
PushComment bool `json:"push_comment"`
|
|
PushMessage bool `json:"push_message"`
|
|
PushMention bool `json:"push_mention"`
|
|
}
|
|
|
|
// GetPreferences returns notification preferences for a user
|
|
func (ns *NotificationService) GetPreferences(userID uuid.UUID) (*NotificationPrefs, error) {
|
|
ctx := context.Background()
|
|
|
|
prefs := &NotificationPrefs{PushFollow: true, PushLike: true, PushComment: true, PushMessage: true, PushMention: true}
|
|
|
|
err := ns.db.QueryRowContext(ctx, `
|
|
SELECT push_follow, push_like, push_comment, push_message, push_mention
|
|
FROM notification_preferences
|
|
WHERE user_id = $1
|
|
`, userID).Scan(&prefs.PushFollow, &prefs.PushLike, &prefs.PushComment, &prefs.PushMessage, &prefs.PushMention)
|
|
|
|
if err == nil {
|
|
return prefs, nil
|
|
}
|
|
if err != sql.ErrNoRows {
|
|
return nil, fmt.Errorf("failed to get preferences: %w", err)
|
|
}
|
|
|
|
return prefs, nil
|
|
}
|
|
|
|
// UpdatePreferences updates notification preferences
|
|
func (ns *NotificationService) UpdatePreferences(userID uuid.UUID, pushFollow, pushLike, pushComment, pushMessage, pushMention *bool) error {
|
|
ctx := context.Background()
|
|
|
|
_, err := ns.db.ExecContext(ctx, `
|
|
INSERT INTO notification_preferences (user_id, push_follow, push_like, push_comment, push_message, push_mention, updated_at)
|
|
VALUES ($1, COALESCE($2, true), COALESCE($3, true), COALESCE($4, true), COALESCE($5, true), COALESCE($6, true), NOW())
|
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
push_follow = CASE WHEN $2 IS NOT NULL THEN $2 ELSE notification_preferences.push_follow END,
|
|
push_like = CASE WHEN $3 IS NOT NULL THEN $3 ELSE notification_preferences.push_like END,
|
|
push_comment = CASE WHEN $4 IS NOT NULL THEN $4 ELSE notification_preferences.push_comment END,
|
|
push_message = CASE WHEN $5 IS NOT NULL THEN $5 ELSE notification_preferences.push_message END,
|
|
push_mention = CASE WHEN $6 IS NOT NULL THEN $6 ELSE notification_preferences.push_mention END,
|
|
updated_at = NOW()
|
|
`, userID, pushFollow, pushLike, pushComment, pushMessage, pushMention)
|
|
|
|
return err
|
|
}
|