veza/veza-backend-api/cmd/tools/seed/seed_social.go
senke 2eff5a9b10 refactor(backend): split seed tool into domain-specific modules
Extract monolithic seed main.go into separate files per domain:
users, tracks, playlists, chat, analytics, marketplace, social,
content, live, moderation, notifications, and misc. Add config,
fake data helpers, and utility modules. Update Makefile targets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 23:35:07 +01:00

248 lines
7.1 KiB
Go

package main
import (
"database/sql"
"fmt"
"time"
)
// SeedSocial creates follows, track_likes, track_reposts, comments, and user_blocks.
func SeedSocial(db *sql.DB, cfg Config, users []SeededUser, tracks []SeededTrack) error {
fmt.Println("\n═══ SOCIAL ═══")
artists := GetArtists(users)
allUsers := users
// ── 1. Follows (scale-free distribution) ─────────────────────────────────
p := NewProgress("follows", cfg.Follows)
followRows := make([][]interface{}, 0, cfg.Follows)
followSeen := make(map[string]bool)
// Build popularity weights: artists are much more likely to be followed
artistSet := make(map[string]bool)
for _, a := range artists {
artistSet[a.ID] = true
}
for i := 0; i < cfg.Follows*2 && len(followRows) < cfg.Follows; i++ {
follower := allUsers[rng.Intn(len(allUsers))]
// 70% chance to follow an artist, 30% random user
var followed SeededUser
if randChance(70) && len(artists) > 0 {
// Power-law: top artists get followed more
idx := PowerLaw(0, len(artists)-1, 1.5)
followed = artists[idx]
} else {
followed = allUsers[rng.Intn(len(allUsers))]
}
if follower.ID == followed.ID {
continue
}
key := follower.ID + ":" + followed.ID
if followSeen[key] {
continue
}
followSeen[key] = true
createdAt := RandomTimeAfter(follower.CreatedAt)
if followed.CreatedAt.After(follower.CreatedAt) {
createdAt = RandomTimeAfter(followed.CreatedAt)
}
followRows = append(followRows, []interface{}{
newUUID(), follower.ID, followed.ID, createdAt, createdAt,
})
}
// Ensure test artist@veza.music has many followers
testArtist := findUser(users, "artist@veza.music")
if testArtist != nil {
for _, u := range users[:min(200, len(users))] {
if u.ID == testArtist.ID {
continue
}
key := u.ID + ":" + testArtist.ID
if followSeen[key] {
continue
}
followSeen[key] = true
followRows = append(followRows, []interface{}{
newUUID(), u.ID, testArtist.ID, RandomTimeAfter(u.CreatedAt), time.Now(),
})
}
}
_, err := BulkInsert(db, "follows",
"id, follower_id, followed_id, created_at, updated_at",
followRows)
if err != nil {
return fmt.Errorf("insert follows: %w", err)
}
p.Update(len(followRows))
p.Done()
// ── 2. Track likes ───────────────────────────────────────────────────────
if len(tracks) == 0 {
return nil
}
p = NewProgress("track_likes", cfg.TrackLikes)
likeRows := make([][]interface{}, 0, cfg.TrackLikes)
likeSeen := make(map[string]bool)
for i := 0; i < cfg.TrackLikes*2 && len(likeRows) < cfg.TrackLikes; i++ {
user := allUsers[rng.Intn(len(allUsers))]
// Power-law: popular tracks get more likes
track := tracks[PowerLaw(0, len(tracks)-1, 1.2)]
key := user.ID + ":" + track.ID
if likeSeen[key] {
continue
}
likeSeen[key] = true
likeRows = append(likeRows, []interface{}{
newUUID(), user.ID, track.ID, time.Now(),
})
}
_, err = BulkInsert(db, "track_likes",
"id, user_id, track_id, created_at",
likeRows)
if err != nil {
return fmt.Errorf("insert track_likes: %w", err)
}
p.Update(len(likeRows))
p.Done()
// ── 3. Track reposts ─────────────────────────────────────────────────────
p = NewProgress("track_reposts", cfg.TrackReposts)
repostRows := make([][]interface{}, 0, cfg.TrackReposts)
repostSeen := make(map[string]bool)
for i := 0; i < cfg.TrackReposts*2 && len(repostRows) < cfg.TrackReposts; i++ {
user := allUsers[rng.Intn(len(allUsers))]
track := tracks[PowerLaw(0, len(tracks)-1, 1.2)]
if user.ID == track.CreatorID {
continue // Don't repost own tracks
}
key := user.ID + ":" + track.ID
if repostSeen[key] {
continue
}
repostSeen[key] = true
repostRows = append(repostRows, []interface{}{
newUUID(), user.ID, track.ID, time.Now(),
})
}
_, err = BulkInsert(db, "track_reposts",
"id, user_id, track_id, created_at",
repostRows)
if err != nil {
return fmt.Errorf("insert track_reposts: %w", err)
}
p.Update(len(repostRows))
p.Done()
// ── 4. Comments (track_comments + generic comments) ──────────────────────
p = NewProgress("track_comments", cfg.Comments)
commentRows := make([][]interface{}, 0, cfg.Comments)
for i := 0; i < cfg.Comments; i++ {
user := allUsers[rng.Intn(len(allUsers))]
track := tracks[PowerLaw(0, len(tracks)-1, 1.0)]
content := GenComment()
createdAt := RandomTimeAfter(user.CreatedAt)
// Timed comment: position within the track
var timestampSec *int
if randChance(40) {
ts := randInt(0, track.Duration)
timestampSec = &ts
}
commentRows = append(commentRows, []interface{}{
newUUID(), track.ID, user.ID, content,
nil, // parent_comment_id
timestampSec, false, false, nil,
createdAt, createdAt, nil,
})
}
_, err = BulkInsert(db, "track_comments",
"id, track_id, user_id, content, parent_comment_id, timestamp_seconds, is_edited, is_deleted, parent_id, created_at, updated_at, deleted_at",
commentRows)
if err != nil {
return fmt.Errorf("insert track_comments: %w", err)
}
p.Update(len(commentRows))
p.Done()
// Also populate the generic 'comments' table (used for track comments via target_type)
p = NewProgress("comments (generic)", cfg.Comments/2)
genCommentRows := make([][]interface{}, 0, cfg.Comments/2)
for i := 0; i < cfg.Comments/2; i++ {
user := allUsers[rng.Intn(len(allUsers))]
track := tracks[rng.Intn(len(tracks))]
content := GenComment()
createdAt := RandomTimeAfter(user.CreatedAt)
genCommentRows = append(genCommentRows, []interface{}{
newUUID(), user.ID, track.ID, "track", content, createdAt, createdAt,
})
}
_, _ = BulkInsert(db, "comments",
"id, user_id, target_id, target_type, content, created_at, updated_at",
genCommentRows)
p.Update(len(genCommentRows))
p.Done()
// ── 5. User blocks (small number) ────────────────────────────────────────
blockCount := len(users) / 50 // ~2% of users have blocked someone
if blockCount < 5 {
blockCount = 5
}
p = NewProgress("user_blocks", blockCount)
blockRows := make([][]interface{}, 0, blockCount)
blockSeen := make(map[string]bool)
for i := 0; i < blockCount; i++ {
blocker := allUsers[rng.Intn(len(allUsers))]
blocked := allUsers[rng.Intn(len(allUsers))]
if blocker.ID == blocked.ID {
continue
}
key := blocker.ID + ":" + blocked.ID
if blockSeen[key] {
continue
}
blockSeen[key] = true
blockRows = append(blockRows, []interface{}{
newUUID(), blocker.ID, blocked.ID, nil, time.Now(), time.Now(),
})
}
_, _ = BulkInsert(db, "user_blocks",
"id, blocker_id, blocked_id, reason, created_at, updated_at",
blockRows)
p.Update(len(blockRows))
p.Done()
return nil
}
func findUser(users []SeededUser, email string) *SeededUser {
for i := range users {
if users[i].Email == email {
return &users[i]
}
}
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}