veza/veza-backend-api/internal/services/social_service.go

315 lines
7.6 KiB
Go
Raw Normal View History

2025-12-03 19:29:37 +00:00
package services
import (
"context"
"database/sql"
"fmt"
2025-12-03 19:29:37 +00:00
"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 {
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
2026-03-06 18:13:16 +00:00
func (ss *SocialService) FollowUser(ctx context.Context, followerID, followedID uuid.UUID) error {
2025-12-03 19:29:37 +00:00
_, 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",
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
2026-03-06 18:13:16 +00:00
func (ss *SocialService) UnfollowUser(ctx context.Context, followerID, followedID uuid.UUID) error {
2025-12-03 19:29:37 +00:00
_, 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
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
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
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()
var commentID uuid.UUID
2025-12-03 19:29:37 +00:00
err := ss.db.QueryRowContext(ctx, `
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
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
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
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
}
// BlockUser creates a block relationship between users
// BE-API-018: Implement user block/unblock endpoints
func (ss *SocialService) BlockUser(blockerID, blockedID uuid.UUID) error {
ctx := context.Background()
// Vérifier qu'on ne peut pas se bloquer soi-même
if blockerID == blockedID {
return fmt.Errorf("cannot block yourself")
}
_, err := ss.db.ExecContext(ctx, `
INSERT INTO user_blocks (blocker_id, blocked_id)
VALUES ($1, $2)
ON CONFLICT (blocker_id, blocked_id) DO NOTHING
`, blockerID, blockedID)
if err != nil {
return fmt.Errorf("failed to block user: %w", err)
}
ss.logger.Info("User blocked",
zap.String("blocker_id", blockerID.String()),
zap.String("blocked_id", blockedID.String()),
)
return nil
}
// UnblockUser removes a block relationship between users
// BE-API-018: Implement user block/unblock endpoints
func (ss *SocialService) UnblockUser(blockerID, blockedID uuid.UUID) error {
ctx := context.Background()
_, err := ss.db.ExecContext(ctx, `
DELETE FROM user_blocks
WHERE blocker_id = $1 AND blocked_id = $2
`, blockerID, blockedID)
if err != nil {
return fmt.Errorf("failed to unblock user: %w", err)
}
ss.logger.Info("User unblocked",
zap.String("blocker_id", blockerID.String()),
zap.String("blocked_id", blockedID.String()),
)
return nil
}
// IsBlocked checks if a user is blocked by another user
// BE-API-018: Helper method to check block status
func (ss *SocialService) IsBlocked(blockerID, blockedID uuid.UUID) (bool, error) {
ctx := context.Background()
var exists bool
err := ss.db.QueryRowContext(ctx, `
SELECT EXISTS(
SELECT 1 FROM user_blocks
WHERE blocker_id = $1 AND blocked_id = $2
)
`, blockerID, blockedID).Scan(&exists)
if err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, fmt.Errorf("failed to check block status: %w", err)
}
return exists, nil
}