Update RabbitMQ config and eventbus. Improve secret filter logging. Refine presence, cloud, and social services. Update announcement and feature flag handlers. Add track_likes updated_at migration. Rebuild seed binary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
"veza-backend-api/internal/models"
|
|
)
|
|
|
|
// PresenceService manages user presence (v0.301 Lot P1)
|
|
type PresenceService struct {
|
|
db *gorm.DB
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewPresenceService creates a new PresenceService
|
|
func NewPresenceService(db *gorm.DB, logger *zap.Logger) *PresenceService {
|
|
return &PresenceService{db: db, logger: logger}
|
|
}
|
|
|
|
// UpdatePresenceInput holds optional fields for presence update (P2)
|
|
type UpdatePresenceInput struct {
|
|
Status *string
|
|
StatusMsg *string
|
|
TrackID *uuid.UUID
|
|
TrackTitle *string
|
|
Invisible *bool
|
|
}
|
|
|
|
// UpdatePresence updates or creates presence for the current user (call on each authenticated request)
|
|
// P2: Extended with status_message, track_id, track_title, invisible
|
|
func (s *PresenceService) UpdatePresence(ctx context.Context, userID uuid.UUID, status string) error {
|
|
return s.UpdatePresenceFull(ctx, userID, &UpdatePresenceInput{Status: &status})
|
|
}
|
|
|
|
// UpdatePresenceFull updates presence with optional fields (P2)
|
|
// Uses INSERT ... ON CONFLICT DO UPDATE for atomic upsert, safe for concurrent
|
|
// calls from auth middleware goroutine + explicit PUT /users/me/presence.
|
|
func (s *PresenceService) UpdatePresenceFull(ctx context.Context, userID uuid.UUID, input *UpdatePresenceInput) error {
|
|
if input == nil {
|
|
return s.UpdatePresence(ctx, userID, "online")
|
|
}
|
|
now := time.Now()
|
|
|
|
status := "online"
|
|
if input.Status != nil {
|
|
status = *input.Status
|
|
}
|
|
p := models.UserPresence{
|
|
UserID: userID,
|
|
Status: status,
|
|
LastSeenAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if input.StatusMsg != nil {
|
|
p.StatusMsg = *input.StatusMsg
|
|
}
|
|
if input.TrackID != nil {
|
|
p.TrackID = input.TrackID
|
|
}
|
|
if input.TrackTitle != nil {
|
|
p.TrackTitle = *input.TrackTitle
|
|
}
|
|
if input.Invisible != nil {
|
|
p.Invisible = *input.Invisible
|
|
}
|
|
|
|
return s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "user_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{
|
|
"status", "status_message", "track_id", "track_title",
|
|
"invisible", "last_seen_at", "updated_at",
|
|
}),
|
|
}).Create(&p).Error
|
|
}
|
|
|
|
// GetPresence returns presence for a user, or nil if not found
|
|
// P2: If user is invisible, returns offline for others (caller must pass requestUserID to check)
|
|
func (s *PresenceService) GetPresence(ctx context.Context, userID uuid.UUID) (*models.UserPresence, error) {
|
|
return s.GetPresenceForViewer(ctx, userID, nil)
|
|
}
|
|
|
|
// GetPresenceForViewer returns presence for a user as seen by another user
|
|
// P2: If target is invisible and viewer is not self, returns offline
|
|
func (s *PresenceService) GetPresenceForViewer(ctx context.Context, userID uuid.UUID, viewerID *uuid.UUID) (*models.UserPresence, error) {
|
|
var p models.UserPresence
|
|
err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&p).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// P2: If invisible and viewer is someone else, return offline
|
|
if p.Invisible && viewerID != nil && *viewerID != userID {
|
|
return &models.UserPresence{
|
|
UserID: p.UserID,
|
|
Status: "offline",
|
|
LastSeenAt: p.LastSeenAt,
|
|
UpdatedAt: p.UpdatedAt,
|
|
}, nil
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
// GetOnlineUsers returns user IDs with status online (for list in chat/sidebar)
|
|
func (s *PresenceService) GetOnlineUsers(ctx context.Context, limit int) ([]uuid.UUID, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
var presences []models.UserPresence
|
|
err := s.db.WithContext(ctx).Where("status = ?", "online").Order("updated_at DESC").Limit(limit).Find(&presences).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids := make([]uuid.UUID, 0, len(presences))
|
|
for _, p := range presences {
|
|
ids = append(ids, p.UserID)
|
|
}
|
|
return ids, nil
|
|
}
|