2025-12-03 19:29:37 +00:00
package services
import (
"context"
"errors"
"time"
"veza-backend-api/internal/models"
2026-03-05 22:03:43 +00:00
"github.com/google/uuid"
2025-12-03 19:29:37 +00:00
"go.uber.org/zap"
"gorm.io/gorm"
)
type CommentService struct {
2026-04-14 10:22:14 +00:00
db * gorm . DB
logger * zap . Logger
moderationService * CommentModerationService // v0.10.3 F201: optional keyword moderation
2025-12-03 19:29:37 +00:00
}
func NewCommentService ( db * gorm . DB , logger * zap . Logger ) * CommentService {
return & CommentService {
db : db ,
logger : logger ,
}
}
feat(v0.10.3): Commentaires & Interactions Sociales - F201-F215
- F201: Commentaires avec timestamp cliquable, modération mots-clés
- F202: Likes privés (compteur visible créateur uniquement)
- F203: Reposts de tracks sur le profil, bouton Repost, onglet Reposts
- F204: Notifications (commentaire, repost), pas de gamification
Backend: migrations 127/128, comment_moderation_service, track_repost_service,
GetTrackLikes/GetTrack masquent like_count pour non-créateurs
Frontend: LikeButton isCreator, RepostButton, Reposts tab profil, timestamp seek
2026-03-09 09:30:47 +00:00
// SetModerationService sets the optional moderation service for comment content (v0.10.3 F201).
func ( s * CommentService ) SetModerationService ( mod * CommentModerationService ) {
s . moderationService = mod
}
2025-12-03 19:29:37 +00:00
// CreateComment creates a new comment on a track
func ( s * CommentService ) CreateComment ( ctx context . Context , trackID uuid . UUID , userID uuid . UUID , content string , timestamp float64 , parentID * uuid . UUID ) ( * models . TrackComment , error ) { // Updated trackID and parentID to uuid.UUID
feat(v0.10.3): Commentaires & Interactions Sociales - F201-F215
- F201: Commentaires avec timestamp cliquable, modération mots-clés
- F202: Likes privés (compteur visible créateur uniquement)
- F203: Reposts de tracks sur le profil, bouton Repost, onglet Reposts
- F204: Notifications (commentaire, repost), pas de gamification
Backend: migrations 127/128, comment_moderation_service, track_repost_service,
GetTrackLikes/GetTrack masquent like_count pour non-créateurs
Frontend: LikeButton isCreator, RepostButton, Reposts tab profil, timestamp seek
2026-03-09 09:30:47 +00:00
// v0.10.3 F201: Moderation by keyword list (deterministic, no ML)
if s . moderationService != nil {
banned , err := s . moderationService . ContainsBannedKeyword ( ctx , content )
if err != nil {
s . logger . Warn ( "moderation check failed" , zap . Error ( err ) )
// Continue without moderation on error (fail-open for availability)
} else if banned {
return nil , ErrModerationRejected
}
}
2025-12-03 19:29:37 +00:00
// Verify if track exists
var track models . Track
if err := s . db . WithContext ( ctx ) . First ( & track , "id = ?" , trackID ) . Error ; err != nil { // Updated query
if errors . Is ( err , gorm . ErrRecordNotFound ) {
2025-12-06 16:21:59 +00:00
return nil , ErrTrackNotFound
2025-12-03 19:29:37 +00:00
}
return nil , err
}
// Verify if parent comment exists (if reply)
if parentID != nil {
var parent models . TrackComment
if err := s . db . WithContext ( ctx ) . First ( & parent , "id = ?" , * parentID ) . Error ; err != nil { // Updated query
if errors . Is ( err , gorm . ErrRecordNotFound ) {
2025-12-06 16:21:59 +00:00
return nil , ErrParentCommentNotFound
2025-12-03 19:29:37 +00:00
}
return nil , err
}
// Ensure parent belongs to the same track
if parent . TrackID != trackID {
2025-12-06 16:21:59 +00:00
return nil , ErrParentTrackMismatch
2025-12-03 19:29:37 +00:00
}
}
comment := & models . TrackComment {
TrackID : trackID ,
UserID : userID ,
Content : content ,
Timestamp : timestamp ,
ParentID : parentID ,
CreatedAt : time . Now ( ) ,
UpdatedAt : time . Now ( ) ,
}
if err := s . db . WithContext ( ctx ) . Create ( comment ) . Error ; err != nil {
s . logger . Error ( "Failed to create comment" , zap . Error ( err ) )
return nil , err
}
// Preload user info for response
if err := s . db . WithContext ( ctx ) . Preload ( "User" ) . First ( comment , comment . ID ) . Error ; err != nil {
return comment , nil // Return comment without user info if preload fails
}
s . logger . Info ( "Comment created" ,
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
zap . Any ( "comment_id" , comment . ID ) ,
zap . Any ( "track_id" , trackID ) ,
2025-12-03 19:29:37 +00:00
zap . String ( "user_id" , userID . String ( ) ) )
return comment , nil
}
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
// GetTrackCreatorID returns the creator user ID of a track (Phase 2.2)
func ( s * CommentService ) GetTrackCreatorID ( ctx context . Context , trackID uuid . UUID ) ( uuid . UUID , error ) {
var track models . Track
if err := s . db . WithContext ( ctx ) . Select ( "user_id" ) . First ( & track , "id = ?" , trackID ) . Error ; err != nil {
if errors . Is ( err , gorm . ErrRecordNotFound ) {
return uuid . Nil , ErrTrackNotFound
}
return uuid . Nil , err
}
return track . UserID , nil
}
2025-12-03 19:29:37 +00:00
// GetComments retrieves comments for a track
func ( s * CommentService ) GetComments ( ctx context . Context , trackID uuid . UUID , page , limit int ) ( [ ] models . TrackComment , int64 , error ) { // Updated trackID to uuid.UUID
var comments [ ] models . TrackComment
var total int64
offset := ( page - 1 ) * limit
// Count total top-level comments (or all comments? usually top-level for pagination, replies fetched separately or nested)
// Here we fetch all top-level comments
query := s . db . WithContext ( ctx ) . Model ( & models . TrackComment { } ) . Where ( "track_id = ? AND parent_id IS NULL" , trackID )
if err := query . Count ( & total ) . Error ; err != nil {
return nil , 0 , err
}
// Fetch comments with user info and replies
// Note: Deep nesting of replies might require recursive query or multiple queries.
// For simplicity, we just preload direct replies or let frontend handle threading if flat list.
// Assuming flat list of top level + preloaded replies?
// Let's just fetch top level and preload their replies one level deep for now
err := query .
Preload ( "User" ) .
Preload ( "Replies" ) .
Preload ( "Replies.User" ) .
Order ( "created_at DESC" ) .
Limit ( limit ) .
Offset ( offset ) .
Find ( & comments ) . Error
if err != nil {
s . logger . Error ( "Failed to get comments" , zap . Error ( err ) )
return nil , 0 , err
}
return comments , total , nil
}
// UpdateComment updates a comment
func ( s * CommentService ) UpdateComment ( ctx context . Context , commentID uuid . UUID , userID uuid . UUID , content string ) ( * models . TrackComment , error ) { // Updated commentID to uuid.UUID
var comment models . TrackComment
if err := s . db . WithContext ( ctx ) . First ( & comment , "id = ?" , commentID ) . Error ; err != nil { // Updated query
if errors . Is ( err , gorm . ErrRecordNotFound ) {
2025-12-06 16:21:59 +00:00
return nil , ErrCommentNotFound
2025-12-03 19:29:37 +00:00
}
return nil , err
}
// Check permission
if comment . UserID != userID {
2025-12-06 16:21:59 +00:00
return nil , ErrForbidden
2025-12-03 19:29:37 +00:00
}
comment . Content = content
comment . IsEdited = true
comment . UpdatedAt = time . Now ( )
if err := s . db . WithContext ( ctx ) . Save ( & comment ) . Error ; err != nil {
s . logger . Error ( "Failed to update comment" , zap . Error ( err ) )
return nil , err
}
s . logger . Info ( "Comment updated" ,
zap . Any ( "comment_id" , comment . ID ) , // Changed to zap.Any for uuid.UUID
zap . String ( "user_id" , userID . String ( ) ) )
return & comment , nil
}
// GetReplies retrieves replies for a given parent comment ID
func ( s * CommentService ) GetReplies ( ctx context . Context , parentID uuid . UUID , page , limit int ) ( [ ] models . TrackComment , int64 , error ) { // Updated parentID to uuid.UUID
var replies [ ] models . TrackComment
var total int64
offset := ( page - 1 ) * limit
2025-12-06 16:21:59 +00:00
// Verify if parent comment exists
var parent models . TrackComment
if err := s . db . WithContext ( ctx ) . First ( & parent , "id = ?" , parentID ) . Error ; err != nil {
if errors . Is ( err , gorm . ErrRecordNotFound ) {
return nil , 0 , ErrParentCommentNotFound
}
return nil , 0 , err
}
2025-12-03 19:29:37 +00:00
// Count total replies
query := s . db . WithContext ( ctx ) . Model ( & models . TrackComment { } ) . Where ( "parent_id = ?" , parentID )
if err := query . Count ( & total ) . Error ; err != nil {
return nil , 0 , err
}
// Fetch replies with user info
err := query .
Preload ( "User" ) .
Order ( "created_at ASC" ) . // Order by oldest first
Limit ( limit ) .
Offset ( offset ) .
Find ( & replies ) . Error
if err != nil {
s . logger . Error ( "Failed to get replies" , zap . Error ( err ) )
return nil , 0 , err
}
return replies , total , nil
}
// DeleteComment deletes a comment
// MIGRATION UUID: userID migré vers uuid.UUID, commentID reste int64
func ( s * CommentService ) DeleteComment ( ctx context . Context , commentID uuid . UUID , userID uuid . UUID , isAdmin bool ) error { // Updated commentID to uuid.UUID
var comment models . TrackComment
if err := s . db . WithContext ( ctx ) . First ( & comment , "id = ?" , commentID ) . Error ; err != nil { // Updated query
if errors . Is ( err , gorm . ErrRecordNotFound ) {
2025-12-06 16:21:59 +00:00
return ErrCommentNotFound
2025-12-03 19:29:37 +00:00
}
return err
}
// Check permission
if comment . UserID != userID && ! isAdmin {
2025-12-06 16:21:59 +00:00
return ErrForbidden
2025-12-03 19:29:37 +00:00
}
// Soft delete or hard delete? Model has DeletedAt so soft delete
if err := s . db . WithContext ( ctx ) . Delete ( & comment ) . Error ; err != nil {
s . logger . Error ( "Failed to delete comment" , zap . Error ( err ) )
return err
}
return nil
}