A community based topic aggregation platform built on atproto

feat(dev): add local dev aggregator setup script

Creates a test aggregator account on the local PDS for development.
Inserts directly into users/aggregators tables to simulate the full
aggregator registration flow locally.

Usage: go run scripts/setup_dev_aggregator.go

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+200
+200
scripts/setup_dev_aggregator.go
··· 1 + // setup_dev_aggregator.go - Creates a local test aggregator on the local PDS 2 + // 3 + // This script creates an aggregator account on the local PDS for development testing. 4 + // After running, you'll need to: 5 + // 1. Register the aggregator via OAuth UI 6 + // 2. Generate an API key via the createApiKey endpoint 7 + // 8 + // Usage: go run scripts/setup_dev_aggregator.go 9 + package main 10 + 11 + import ( 12 + "bytes" 13 + "context" 14 + "database/sql" 15 + "encoding/json" 16 + "fmt" 17 + "io" 18 + "log" 19 + "net/http" 20 + 21 + _ "github.com/lib/pq" 22 + ) 23 + 24 + const ( 25 + PDSURL = "http://localhost:3001" 26 + DatabaseURL = "postgres://dev_user:dev_password@localhost:5435/coves_dev?sslmode=disable" 27 + ) 28 + 29 + type CreateAccountRequest struct { 30 + Email string `json:"email"` 31 + Handle string `json:"handle"` 32 + Password string `json:"password"` 33 + } 34 + 35 + type CreateAccountResponse struct { 36 + DID string `json:"did"` 37 + Handle string `json:"handle"` 38 + AccessJWT string `json:"accessJwt"` 39 + } 40 + 41 + type CreateSessionRequest struct { 42 + Identifier string `json:"identifier"` 43 + Password string `json:"password"` 44 + } 45 + 46 + type CreateSessionResponse struct { 47 + DID string `json:"did"` 48 + Handle string `json:"handle"` 49 + AccessJWT string `json:"accessJwt"` 50 + } 51 + 52 + func main() { 53 + ctx := context.Background() 54 + 55 + // Configuration 56 + handle := "test-aggregator.local.coves.dev" 57 + email := "test-aggregator@example.com" 58 + password := "test-password-12345" 59 + displayName := "Test Aggregator (Dev)" 60 + 61 + log.Printf("Setting up dev aggregator: %s", handle) 62 + 63 + // Connect to database 64 + db, err := sql.Open("postgres", DatabaseURL) 65 + if err != nil { 66 + log.Fatalf("Failed to connect to database: %v", err) 67 + } 68 + defer db.Close() 69 + 70 + // Step 1: Try to create account on PDS (or get existing session) 71 + log.Printf("Creating account on PDS: %s", PDSURL) 72 + 73 + var did string 74 + 75 + // First try to create account 76 + createResp, err := createAccount(handle, email, password) 77 + if err != nil { 78 + log.Printf("Account creation failed (may already exist): %v", err) 79 + log.Printf("Trying to create session with existing account...") 80 + 81 + // Try to login instead 82 + sessionResp, err := createSession(handle, password) 83 + if err != nil { 84 + log.Fatalf("Failed to create session: %v", err) 85 + } 86 + did = sessionResp.DID 87 + log.Printf("Logged in as existing account: %s", did) 88 + } else { 89 + did = createResp.DID 90 + log.Printf("Created new account: %s", did) 91 + } 92 + 93 + // Step 2: Check if already in users table 94 + var existingHandle string 95 + err = db.QueryRowContext(ctx, "SELECT handle FROM users WHERE did = $1", did).Scan(&existingHandle) 96 + if err == nil { 97 + log.Printf("User already exists in users table: %s", existingHandle) 98 + } else { 99 + // Insert into users table 100 + log.Printf("Inserting user into users table...") 101 + _, err = db.ExecContext(ctx, ` 102 + INSERT INTO users (did, handle, pds_url) 103 + VALUES ($1, $2, $3) 104 + ON CONFLICT (did) DO UPDATE SET handle = $2 105 + `, did, handle, PDSURL) 106 + if err != nil { 107 + log.Fatalf("Failed to insert user: %v", err) 108 + } 109 + } 110 + 111 + // Step 3: Check if already in aggregators table 112 + var existingAggDID string 113 + err = db.QueryRowContext(ctx, "SELECT did FROM aggregators WHERE did = $1", did).Scan(&existingAggDID) 114 + if err == nil { 115 + log.Printf("Aggregator already exists in aggregators table") 116 + } else { 117 + // Insert into aggregators table 118 + log.Printf("Inserting aggregator into aggregators table...") 119 + recordURI := fmt.Sprintf("at://%s/social.coves.aggregator.declaration/self", did) 120 + recordCID := "dev-placeholder-cid" 121 + 122 + _, err = db.ExecContext(ctx, ` 123 + INSERT INTO aggregators (did, display_name, description, record_uri, record_cid, created_at, indexed_at) 124 + VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) 125 + `, did, displayName, "Development test aggregator", recordURI, recordCID) 126 + if err != nil { 127 + log.Fatalf("Failed to insert aggregator: %v", err) 128 + } 129 + } 130 + 131 + fmt.Println() 132 + fmt.Println("========================================") 133 + fmt.Println(" DEV AGGREGATOR ACCOUNT CREATED") 134 + fmt.Println("========================================") 135 + fmt.Println() 136 + fmt.Printf(" DID: %s\n", did) 137 + fmt.Printf(" Handle: %s\n", handle) 138 + fmt.Printf(" Password: %s\n", password) 139 + fmt.Println() 140 + fmt.Println(" Next steps:") 141 + fmt.Println(" 1. Start Coves server: make run") 142 + fmt.Println(" 2. Authenticate as this account via OAuth UI") 143 + fmt.Println(" 3. Call POST /xrpc/social.coves.aggregator.createApiKey") 144 + fmt.Println(" 4. Save the API key and add to aggregators/kagi-news/.env") 145 + fmt.Println() 146 + fmt.Println("========================================") 147 + } 148 + 149 + func createAccount(handle, email, password string) (*CreateAccountResponse, error) { 150 + reqBody := CreateAccountRequest{ 151 + Email: email, 152 + Handle: handle, 153 + Password: password, 154 + } 155 + 156 + body, _ := json.Marshal(reqBody) 157 + resp, err := http.Post(PDSURL+"/xrpc/com.atproto.server.createAccount", "application/json", bytes.NewReader(body)) 158 + if err != nil { 159 + return nil, fmt.Errorf("request failed: %w", err) 160 + } 161 + defer resp.Body.Close() 162 + 163 + respBody, _ := io.ReadAll(resp.Body) 164 + if resp.StatusCode != http.StatusOK { 165 + return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(respBody)) 166 + } 167 + 168 + var result CreateAccountResponse 169 + if err := json.Unmarshal(respBody, &result); err != nil { 170 + return nil, fmt.Errorf("failed to parse response: %w", err) 171 + } 172 + 173 + return &result, nil 174 + } 175 + 176 + func createSession(identifier, password string) (*CreateSessionResponse, error) { 177 + reqBody := CreateSessionRequest{ 178 + Identifier: identifier, 179 + Password: password, 180 + } 181 + 182 + body, _ := json.Marshal(reqBody) 183 + resp, err := http.Post(PDSURL+"/xrpc/com.atproto.server.createSession", "application/json", bytes.NewReader(body)) 184 + if err != nil { 185 + return nil, fmt.Errorf("request failed: %w", err) 186 + } 187 + defer resp.Body.Close() 188 + 189 + respBody, _ := io.ReadAll(resp.Body) 190 + if resp.StatusCode != http.StatusOK { 191 + return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(respBody)) 192 + } 193 + 194 + var result CreateSessionResponse 195 + if err := json.Unmarshal(respBody, &result); err != nil { 196 + return nil, fmt.Errorf("failed to parse response: %w", err) 197 + } 198 + 199 + return &result, nil 200 + }