2025-12-03 19:29:37 +00:00
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
|
|
|
|
"veza-backend-api/internal/database"
|
|
|
|
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SocialService handles social features (follows, likes, comments)
|
|
|
|
|
type SocialService struct {
|
|
|
|
|
db *database.Database
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Comment represents a comment on a track
|
|
|
|
|
type Comment struct {
|
2025-12-04 01:15:48 +00:00
|
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
|
|
|
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
|
|
|
|
TrackID uuid.UUID `json:"track_id" db:"track_id"`
|
|
|
|
|
ParentID *uuid.UUID `json:"parent_id" db:"parent_id"`
|
|
|
|
|
Content string `json:"content" db:"content"`
|
|
|
|
|
CreatedAt string `json:"created_at" db:"created_at"`
|
|
|
|
|
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewSocialService creates a new social service
|
|
|
|
|
func NewSocialService(db *database.Database, logger *zap.Logger) *SocialService {
|
|
|
|
|
return &SocialService{
|
|
|
|
|
db: db,
|
|
|
|
|
logger: logger,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FollowUser creates a follow relationship
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) FollowUser(followerID, followedID uuid.UUID) error {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
_, err := ss.db.ExecContext(ctx, `
|
|
|
|
|
INSERT INTO follows (follower_id, followed_id)
|
|
|
|
|
VALUES ($1, $2)
|
|
|
|
|
ON CONFLICT (follower_id, followed_id) DO NOTHING
|
|
|
|
|
`, followerID, followedID)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to follow user: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ss.logger.Info("User followed",
|
2025-12-04 01:15:48 +00:00
|
|
|
zap.String("follower_id", followerID.String()),
|
|
|
|
|
zap.String("followed_id", followedID.String()),
|
2025-12-03 19:29:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UnfollowUser removes a follow relationship
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) UnfollowUser(followerID, followedID uuid.UUID) error {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
_, err := ss.db.ExecContext(ctx, `
|
|
|
|
|
DELETE FROM follows
|
|
|
|
|
WHERE follower_id = $1 AND followed_id = $2
|
|
|
|
|
`, followerID, followedID)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to unfollow user: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LikeTrack creates a like on a track
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) LikeTrack(userID, trackID uuid.UUID) error {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
_, err := ss.db.ExecContext(ctx, `
|
|
|
|
|
INSERT INTO likes (user_id, track_id)
|
|
|
|
|
VALUES ($1, $2)
|
|
|
|
|
ON CONFLICT (user_id, track_id) DO NOTHING
|
|
|
|
|
`, userID, trackID)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to like track: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UnlikeTrack removes a like from a track
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) UnlikeTrack(userID, trackID uuid.UUID) error {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
_, err := ss.db.ExecContext(ctx, `
|
|
|
|
|
DELETE FROM likes
|
|
|
|
|
WHERE user_id = $1 AND track_id = $2
|
|
|
|
|
`, userID, trackID)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to unlike track: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateComment creates a comment on a track
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) CreateComment(userID, trackID uuid.UUID, content string, parentID *uuid.UUID) (*Comment, error) {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
2025-12-04 01:15:48 +00:00
|
|
|
var commentID uuid.UUID
|
2025-12-03 19:29:37 +00:00
|
|
|
err := ss.db.QueryRowContext(ctx, `
|
2025-12-04 01:15:48 +00:00
|
|
|
INSERT INTO comments (id, user_id, track_id, parent_id, content)
|
|
|
|
|
VALUES (gen_random_uuid(), $1, $2, $3, $4)
|
2025-12-03 19:29:37 +00:00
|
|
|
RETURNING id
|
|
|
|
|
`, userID, trackID, parentID, content).Scan(&commentID)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create comment: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch and return the created comment
|
|
|
|
|
var comment Comment
|
|
|
|
|
err = ss.db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT id, user_id, track_id, parent_id, content, created_at, updated_at
|
|
|
|
|
FROM comments
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
`, commentID).Scan(
|
|
|
|
|
&comment.ID,
|
|
|
|
|
&comment.UserID,
|
|
|
|
|
&comment.TrackID,
|
|
|
|
|
&comment.ParentID,
|
|
|
|
|
&comment.Content,
|
|
|
|
|
&comment.CreatedAt,
|
|
|
|
|
&comment.UpdatedAt,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to fetch comment: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &comment, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetFollowersCount returns the number of followers for a user
|
|
|
|
|
func (ss *SocialService) GetFollowersCount(userID uuid.UUID) (int, error) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
var count int
|
|
|
|
|
err := ss.db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT COUNT(*)
|
|
|
|
|
FROM follows
|
|
|
|
|
WHERE followed_id = $1
|
|
|
|
|
`, userID).Scan(&count)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("failed to get followers count: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return count, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetFollowingCount returns the number of users being followed
|
|
|
|
|
func (ss *SocialService) GetFollowingCount(userID uuid.UUID) (int, error) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
var count int
|
|
|
|
|
err := ss.db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT COUNT(*)
|
|
|
|
|
FROM follows
|
|
|
|
|
WHERE follower_id = $1
|
|
|
|
|
`, userID).Scan(&count)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("failed to get following count: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return count, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetLikesCount returns the number of likes for a track
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) GetLikesCount(trackID uuid.UUID) (int, error) {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
var count int
|
|
|
|
|
err := ss.db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT COUNT(*)
|
|
|
|
|
FROM likes
|
|
|
|
|
WHERE track_id = $1
|
|
|
|
|
`, trackID).Scan(&count)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("failed to get likes count: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return count, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsFollowing checks if a user is following another user
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) IsFollowing(followerID, followedID uuid.UUID) (bool, error) {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
var exists bool
|
|
|
|
|
err := ss.db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT EXISTS(
|
|
|
|
|
SELECT 1 FROM follows
|
|
|
|
|
WHERE follower_id = $1 AND followed_id = $2
|
|
|
|
|
)
|
|
|
|
|
`, followerID, followedID).Scan(&exists)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
return false, fmt.Errorf("failed to check follow status: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return exists, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsTrackLiked checks if a user has liked a track
|
2025-12-04 01:15:48 +00:00
|
|
|
func (ss *SocialService) IsTrackLiked(userID, trackID uuid.UUID) (bool, error) {
|
2025-12-03 19:29:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
var exists bool
|
|
|
|
|
err := ss.db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT EXISTS(
|
|
|
|
|
SELECT 1 FROM likes
|
|
|
|
|
WHERE user_id = $1 AND track_id = $2
|
|
|
|
|
)
|
|
|
|
|
`, userID, trackID).Scan(&exists)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
return false, fmt.Errorf("failed to check like status: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return exists, nil
|
2025-12-04 01:15:48 +00:00
|
|
|
}
|