// seed.go is a standalone tool that populates an OwnCord database with // development data (users, channels, messages, DMs). It is idempotent: // running it multiple times against the same database is safe. // // Usage: // // go run scripts/seed.go # uses ./data/chatserver.db // go run scripts/seed.go -db path/to/owncord.db # custom path package main import ( "context" "flag" "fmt" "log" "os" "github.com/owncord/server/auth" "github.com/owncord/server/db" ) // ─── Seed data definitions ────────────────────────────────────────────────── // seedUser defines a user to create during seeding. type seedUser struct { Username string Password string RoleID int // 1=Owner, 2=Admin, 3=Moderator, 4=Member } // seedChannel defines a channel to create during seeding. type seedChannel struct { Name string Type string // "text" or "voice" Category string Topic string Position int } // seedMessage defines a message to insert during seeding. // ChannelIdx and UserIdx refer to zero-based indices into the channels and // users slices (resolved after creation). type seedMessage struct { ChannelIdx int UserIdx int Content string } var seedUsers = []seedUser{ {Username: "admin", Password: "admin123", RoleID: 1}, {Username: "alice", Password: "password123", RoleID: 4}, {Username: "bob", Password: "password123", RoleID: 4}, {Username: "charlie", Password: "password123", RoleID: 4}, } var seedChannels = []seedChannel{ {Name: "general", Type: "text", Category: "Text Channels", Topic: "General chat", Position: 0}, {Name: "random", Type: "text", Category: "Text Channels", Topic: "Off-topic discussion", Position: 1}, {Name: "gaming", Type: "text", Category: "Text Channels", Topic: "Gaming talk", Position: 2}, {Name: "Voice Lounge", Type: "voice", Category: "Voice Channels", Position: 3}, {Name: "Gaming Voice", Type: "voice", Category: "Voice Channels", Position: 4}, } // Channel indices for readability. const ( chGeneral = 0 chRandom = 1 chGaming = 2 ) // User indices for readability. const ( uAdmin = 0 uAlice = 1 uBob = 2 uCharlie = 3 ) var seedMessages = []seedMessage{ // #general {chGeneral, uAdmin, "Welcome to OwnCord! This is the general channel."}, {chGeneral, uAlice, "Hey everyone! Glad to be here."}, {chGeneral, uBob, "Hello! This looks great."}, {chGeneral, uCharlie, "Hi all, what's everyone up to?"}, {chGeneral, uAdmin, "Feel free to chat about anything here."}, {chGeneral, uAlice, "Anyone tried the voice chat yet?"}, {chGeneral, uBob, "Not yet, but I'm about to!"}, {chGeneral, uCharlie, "The UI looks really clean."}, {chGeneral, uAdmin, "Thanks! We've been working hard on it."}, {chGeneral, uAlice, "Can we customize themes?"}, {chGeneral, uAdmin, "Yes! Check the settings panel."}, // #random {chRandom, uBob, "Random thought: pineapple on pizza is underrated."}, {chRandom, uCharlie, "Hard disagree, but I respect your opinion."}, {chRandom, uAlice, "Let's not start a war here lol"}, {chRandom, uBob, "Too late, the war has begun!"}, {chRandom, uAdmin, "Keep it friendly, folks!"}, {chRandom, uCharlie, "Anyone watching any good shows lately?"}, {chRandom, uAlice, "I just finished a great series, highly recommend it."}, {chRandom, uBob, "What series?"}, {chRandom, uAlice, "I'll share the link later!"}, // #gaming {chGaming, uCharlie, "Anyone up for some co-op tonight?"}, {chGaming, uBob, "I'm down! What game?"}, {chGaming, uCharlie, "Thinking something chill, maybe Minecraft?"}, {chGaming, uAlice, "Count me in!"}, {chGaming, uAdmin, "I might join later if I finish some work."}, {chGaming, uBob, "No pressure, we'll be on for a while."}, } // seedDMMessages are messages exchanged in the admin<->alice DM channel. var seedDMMessages = []struct { FromIdx int // index into seedUsers Content string }{ {uAdmin, "Hey Alice, welcome to the server!"}, {uAlice, "Thanks! Everything looks awesome."}, {uAdmin, "Let me know if you run into any issues."}, {uAlice, "Will do! One question: how do I change my avatar?"}, {uAdmin, "Go to Settings > Account, you can upload one there."}, } // ─── Main ─────────────────────────────────────────────────────────────────── func main() { dbPath := flag.String("db", "data/chatserver.db", "path to the SQLite database file") confirmDev := flag.Bool("confirm-dev", false, "confirm this is a development database (required)") flag.Parse() if !*confirmDev { fmt.Fprintln(os.Stderr, "⚠ This script creates users with weak passwords.") fmt.Fprintln(os.Stderr, " Pass -confirm-dev to confirm this is a development database.") os.Exit(1) } log.SetFlags(0) // no timestamp prefix — keep output clean database, err := db.Open(*dbPath) if err != nil { log.Fatalf("failed to open database at %s: %v", *dbPath, err) } exitCode := run(database) _ = database.Close() os.Exit(exitCode) } func run(database *db.DB) int { if err := db.Migrate(database); err != nil { log.Printf("failed to run migrations: %v", err) return 1 } userIDs, err := createUsers(database) if err != nil { log.Printf("failed to create users: %v", err) return 1 } channelIDs, err := createChannels(database) if err != nil { log.Printf("failed to create channels: %v", err) return 1 } msgCount, err := createMessages(database, channelIDs, userIDs) if err != nil { log.Printf("failed to create messages: %v", err) return 1 } dmMsgCount, err := createDMConversation(database, userIDs) if err != nil { log.Printf("failed to create DM conversation: %v", err) return 1 } fmt.Println("--- Seed complete ---") fmt.Printf(" Users: %d\n", len(userIDs)) fmt.Printf(" Channels: %d\n", len(channelIDs)) fmt.Printf(" Messages: %d (channel) + %d (DM) = %d total\n", msgCount, dmMsgCount, msgCount+dmMsgCount) return 0 } // ─── User creation ────────────────────────────────────────────────────────── func createUsers(database *db.DB) ([]int64, error) { ids := make([]int64, len(seedUsers)) for i, su := range seedUsers { existing, err := database.GetUserByUsername(context.Background(), su.Username) if err != nil { return nil, fmt.Errorf("checking user %q: %w", su.Username, err) } if existing != nil { ids[i] = existing.ID fmt.Printf("[skip] user %q already exists (id=%d)\n", su.Username, existing.ID) continue } hash, err := auth.HashPassword(su.Password) if err != nil { return nil, fmt.Errorf("hashing password for %q: %w", su.Username, err) } id, err := database.CreateUser(context.Background(), su.Username, hash, su.RoleID) if err != nil { return nil, fmt.Errorf("creating user %q: %w", su.Username, err) } ids[i] = id roleName := roleNameFromID(su.RoleID) fmt.Printf("[created] user %q (id=%d, role=%s)\n", su.Username, id, roleName) } return ids, nil } // roleNameFromID returns a human-readable role name for display purposes. func roleNameFromID(roleID int) string { switch roleID { case 1: return "owner" case 2: return "admin" case 3: return "moderator" case 4: return "member" default: return fmt.Sprintf("role_%d", roleID) } } // ─── Channel creation ─────────────────────────────────────────────────────── func createChannels(database *db.DB) ([]int64, error) { ids := make([]int64, len(seedChannels)) // Fetch existing channels once to check for duplicates. existing, err := database.ListChannels(context.Background()) if err != nil { return nil, fmt.Errorf("listing channels: %w", err) } existingByName := make(map[string]int64, len(existing)) for i := range existing { existingByName[existing[i].Name] = existing[i].ID } for i, sc := range seedChannels { if id, found := existingByName[sc.Name]; found { ids[i] = id fmt.Printf("[skip] channel %q already exists (id=%d)\n", sc.Name, id) continue } id, err := database.CreateChannel(context.Background(), sc.Name, sc.Type, sc.Category, sc.Topic, sc.Position) if err != nil { return nil, fmt.Errorf("creating channel %q: %w", sc.Name, err) } ids[i] = id fmt.Printf("[created] channel %q (id=%d, type=%s)\n", sc.Name, id, sc.Type) } return ids, nil } // ─── Message creation ─────────────────────────────────────────────────────── func createMessages(database *db.DB, channelIDs, userIDs []int64) (int, error) { created := 0 for _, sm := range seedMessages { channelID := channelIDs[sm.ChannelIdx] userID := userIDs[sm.UserIdx] // Check if this exact message already exists (content + user + channel). exists, err := messageExists(database, channelID, userID, sm.Content) if err != nil { return 0, fmt.Errorf("checking message existence: %w", err) } if exists { continue } if _, err := database.CreateMessage(context.Background(), channelID, userID, sm.Content, nil); err != nil { return 0, fmt.Errorf("creating message in channel %d: %w", channelID, err) } created++ } if created > 0 { fmt.Printf("[created] %d channel messages\n", created) } else { fmt.Println("[skip] channel messages already seeded") } return created, nil } // messageExists checks whether a message with the given content from the given // user already exists in the channel. Used for idempotency. func messageExists(database *db.DB, channelID, userID int64, content string) (bool, error) { var count int err := database.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM messages WHERE channel_id = ? AND user_id = ? AND content = ? AND deleted = 0`, channelID, userID, content, ).Scan(&count) if err != nil { return false, err } return count > 0, nil } // ─── DM conversation ──────────────────────────────────────────────────────── func createDMConversation(database *db.DB, userIDs []int64) (int, error) { adminID := userIDs[uAdmin] aliceID := userIDs[uAlice] ch, isNew, err := database.GetOrCreateDMChannel(context.Background(), adminID, aliceID) if err != nil { return 0, fmt.Errorf("creating DM channel: %w", err) } if isNew { fmt.Printf("[created] DM channel between admin and alice (id=%d)\n", ch.ID) } else { fmt.Printf("[skip] DM channel between admin and alice already exists (id=%d)\n", ch.ID) } created := 0 for _, dm := range seedDMMessages { senderID := userIDs[dm.FromIdx] exists, err := messageExists(database, ch.ID, senderID, dm.Content) if err != nil { return 0, fmt.Errorf("checking DM message existence: %w", err) } if exists { continue } if _, err := database.CreateMessage(context.Background(), ch.ID, senderID, dm.Content, nil); err != nil { return 0, fmt.Errorf("creating DM message: %w", err) } created++ } if created > 0 { fmt.Printf("[created] %d DM messages\n", created) } else { fmt.Println("[skip] DM messages already seeded") } return created, nil } // ─── Ensure data directory exists ─────────────────────────────────────────── func init() { // The default DB path is data/chatserver.db. Ensure the data directory // exists so db.Open doesn't fail on a fresh checkout. if err := os.MkdirAll("data", 0o750); err != nil { log.Printf("warning: could not create data directory: %v", err) } }