package services import ( "context" "time" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" "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) 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() var p models.UserPresence err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&p).Error if err == gorm.ErrRecordNotFound { p = models.UserPresence{ UserID: userID, Status: "online", LastSeenAt: now, UpdatedAt: now, } } if err != nil && err != gorm.ErrRecordNotFound { return err } if input.Status != nil { p.Status = *input.Status } 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 } p.LastSeenAt = now p.UpdatedAt = now if p.UserID == uuid.Nil { p.UserID = userID return s.db.WithContext(ctx).Create(&p).Error } return s.db.WithContext(ctx).Save(&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 }