veza/veza-backend-api/internal/services/presence_service.go
senke 23487d8723 feat: backend — config, handlers, services, logging, migration
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>
2026-03-23 15:46:57 +01:00

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
}