A community based topic aggregation platform built on atproto
1package main
2
3import (
4 "database/sql"
5 "fmt"
6 "log"
7 "math/rand"
8 "time"
9
10 _ "github.com/lib/pq"
11)
12
13// Post URI: at://did:plc:hcuo3qx2lr7h7dquusbeobht/social.coves.community.post/3m4yohkzbkc2b
14// Community DID: did:plc:hcuo3qx2lr7h7dquusbeobht
15// Community Handle: test-usnews.community.coves.social
16
17const (
18 postURI = "at://did:plc:hcuo3qx2lr7h7dquusbeobht/social.coves.community.post/3m4yohkzbkc2b"
19 postCID = "bafyzohran123"
20 communityDID = "did:plc:hcuo3qx2lr7h7dquusbeobht"
21)
22
23type User struct {
24 DID string
25 Handle string
26 Name string
27}
28
29type Comment struct {
30 URI string
31 CID string
32 RKey string
33 DID string
34 RootURI string
35 RootCID string
36 ParentURI string
37 ParentCID string
38 Content string
39 CreatedAt time.Time
40}
41
42var userNames = []string{
43 "sarah_jenkins", "michael_chen", "jessica_rodriguez", "david_nguyen",
44 "emily_williams", "james_patel", "ashley_garcia", "robert_kim",
45 "jennifer_lee", "william_martinez", "amanda_johnson", "daniel_brown",
46 "melissa_davis", "christopher_wilson", "rebecca_anderson", "matthew_taylor",
47 "laura_thomas", "anthony_moore", "stephanie_jackson", "joshua_white",
48 "nicole_harris", "ryan_martin", "rachel_thompson", "kevin_garcia",
49 "michelle_robinson", "brandon_clark", "samantha_lewis", "justin_walker",
50 "kimberly_hall", "tyler_allen", "brittany_young", "andrew_king",
51}
52
53var positiveComments = []string{
54 "This is such fantastic news! Zohran represents real progressive values and I couldn't be happier with this outcome!",
55 "Finally! A mayor who actually understands the needs of working families. This is a historic moment for NYC!",
56 "What an incredible victory! Zohran's grassroots campaign shows that people power still matters in politics.",
57 "I'm so proud of our city today. This win gives me hope for the future of progressive politics!",
58 "This is exactly what NYC needed. Zohran's policies on housing and healthcare are going to transform our city!",
59 "Congratulations to Zohran! His commitment to affordable housing is going to make such a difference.",
60 "I've been following his campaign since day one and I'm thrilled to see him win. He truly deserves this!",
61 "This victory is proof that authentic progressive candidates can win. So excited for what's ahead!",
62 "Zohran's dedication to public transit and climate action is exactly what we need. Great day for NYC!",
63 "What a momentous occasion! His policies on education are going to help so many families.",
64 "I'm emotional reading this! Zohran gives me so much hope for the direction of our city.",
65 "This is the change we've been waiting for! Can't wait to see his vision become reality.",
66 "His campaign was inspiring from start to finish. This win is well-deserved!",
67 "Finally, a mayor who will prioritize working people over corporate interests!",
68 "The grassroots organizing that made this happen was incredible to witness. Democracy in action!",
69 "Zohran's focus on social justice is refreshing. This is a win for all New Yorkers!",
70 "I volunteered for his campaign and this victory means everything. So proud!",
71 "This gives me faith in our democratic process. People-powered campaigns can still win!",
72 "His policies on criminal justice reform are exactly what NYC needs right now.",
73 "What an amazing day for progressive politics! Zohran is going to do great things.",
74}
75
76var replyComments = []string{
77 "Absolutely agree! This is going to be transformative.",
78 "Couldn't have said it better myself!",
79 "Yes! This is exactly right.",
80 "100% this! So well said.",
81 "This perfectly captures how I feel too!",
82 "Exactly my thoughts! Great perspective.",
83 "So true! I'm equally excited.",
84 "Well put! I share your optimism.",
85 "This! Absolutely this!",
86 "I feel the same way! Great comment.",
87 "You took the words right out of my mouth!",
88 "Perfectly stated! I agree completely.",
89 "Yes yes yes! This is it exactly.",
90 "This is spot on! Thank you for sharing.",
91 "I couldn't agree more with this take!",
92 "Exactly what I was thinking! Well said.",
93 "This captures it perfectly!",
94 "So much this! Great comment.",
95 "You nailed it! I feel exactly the same.",
96 "This is the best take I've seen! Agreed!",
97}
98
99var deepReplyComments = []string{
100 "And it's not just about the policies, it's about the movement he's building!",
101 "This thread is giving me life! So glad to see so many people excited.",
102 "I love seeing all this positive energy! We're going to change NYC together.",
103 "Reading these comments makes me even more hopeful. We did this!",
104 "The solidarity in this thread is beautiful. This is what democracy looks like!",
105 "I'm so grateful to be part of this community right now. Historic moment!",
106 "This conversation shows how ready people are for real change.",
107 "Seeing this support gives me so much hope for what's possible.",
108 "This is the kind of energy we need to keep up! Let's go!",
109 "I'm saving this thread to look back on this incredible moment.",
110}
111
112func generateTID() string {
113 // Simple TID generator for testing (timestamp in microseconds + random)
114 now := time.Now().UnixMicro()
115 return fmt.Sprintf("%d%04d", now, rand.Intn(10000))
116}
117
118func createUser(db *sql.DB, handle, name string, idx int) (*User, error) {
119 did := fmt.Sprintf("did:plc:testuser%d%d", time.Now().Unix(), idx)
120 user := &User{
121 DID: did,
122 Handle: handle,
123 Name: name,
124 }
125
126 query := `
127 INSERT INTO users (did, handle, pds_url, created_at, updated_at)
128 VALUES ($1, $2, $3, NOW(), NOW())
129 ON CONFLICT (did) DO NOTHING
130 `
131
132 _, err := db.Exec(query, user.DID, user.Handle, "http://localhost:3001")
133 if err != nil {
134 return nil, fmt.Errorf("failed to create user: %w", err)
135 }
136
137 log.Printf("Created user: %s (%s)", user.Handle, user.DID)
138 return user, nil
139}
140
141func createComment(db *sql.DB, user *User, content, parentURI, parentCID string, createdAt time.Time) (*Comment, error) {
142 rkey := generateTID()
143 uri := fmt.Sprintf("at://%s/social.coves.community.comment/%s", user.DID, rkey)
144 cid := fmt.Sprintf("bafy%s", rkey)
145
146 comment := &Comment{
147 URI: uri,
148 CID: cid,
149 RKey: rkey,
150 DID: user.DID,
151 RootURI: postURI,
152 RootCID: postCID,
153 ParentURI: parentURI,
154 ParentCID: parentCID,
155 Content: content,
156 CreatedAt: createdAt,
157 }
158
159 query := `
160 INSERT INTO comments (
161 uri, cid, rkey, commenter_did, root_uri, root_cid,
162 parent_uri, parent_cid, content, created_at, indexed_at
163 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
164 ON CONFLICT (uri) DO NOTHING
165 RETURNING id
166 `
167
168 var id int64
169 err := db.QueryRow(query,
170 comment.URI, comment.CID, comment.RKey, comment.DID,
171 comment.RootURI, comment.RootCID, comment.ParentURI, comment.ParentCID,
172 comment.Content, comment.CreatedAt,
173 ).Scan(&id)
174 if err != nil {
175 return nil, fmt.Errorf("failed to create comment: %w", err)
176 }
177
178 log.Printf("Created comment by %s: %.50s...", user.Handle, content)
179 return comment, nil
180}
181
182func updateCommentCount(db *sql.DB, parentURI string, isPost bool) error {
183 if isPost {
184 _, err := db.Exec(`
185 UPDATE posts
186 SET comment_count = comment_count + 1
187 WHERE uri = $1
188 `, parentURI)
189 return err
190 }
191
192 _, err := db.Exec(`
193 UPDATE comments
194 SET reply_count = reply_count + 1
195 WHERE uri = $1
196 `, parentURI)
197 return err
198}
199
200func main() {
201 // Connect to dev database
202 dbURL := "postgres://dev_user:dev_password@localhost:5435/coves_dev?sslmode=disable"
203 db, err := sql.Open("postgres", dbURL)
204 if err != nil {
205 log.Fatalf("Failed to connect to database: %v", err)
206 }
207 defer db.Close()
208
209 if err := db.Ping(); err != nil {
210 log.Fatalf("Failed to ping database: %v", err)
211 }
212
213 log.Println("Connected to database successfully!")
214 log.Printf("Post URI: %s", postURI)
215 log.Println("Starting to generate test data...")
216
217 rand.Seed(time.Now().UnixNano())
218
219 // Create users
220 log.Println("\n=== Creating Users ===")
221 users := make([]*User, 0, len(userNames))
222 for i, name := range userNames {
223 handle := fmt.Sprintf("%s.bsky.social", name)
224 user, err := createUser(db, handle, name, i)
225 if err != nil {
226 log.Printf("Warning: Failed to create user %s: %v", name, err)
227 continue
228 }
229 users = append(users, user)
230 }
231
232 log.Printf("\nCreated %d users", len(users))
233
234 // Generate comments with varied timing
235 log.Println("\n=== Creating Top-Level Comments ===")
236 baseTime := time.Now().Add(-2 * time.Hour) // Comments from 2 hours ago
237 topLevelComments := make([]*Comment, 0)
238
239 // Create 15-20 top-level comments
240 numTopLevel := 15 + rand.Intn(6)
241 for i := 0; i < numTopLevel && i < len(users); i++ {
242 user := users[i]
243 content := positiveComments[i%len(positiveComments)]
244 createdAt := baseTime.Add(time.Duration(i*5+rand.Intn(3)) * time.Minute)
245
246 comment, err := createComment(db, user, content, postURI, postCID, createdAt)
247 if err != nil {
248 log.Printf("Warning: Failed to create top-level comment: %v", err)
249 continue
250 }
251
252 topLevelComments = append(topLevelComments, comment)
253
254 // Update post comment count
255 if err := updateCommentCount(db, postURI, true); err != nil {
256 log.Printf("Warning: Failed to update post comment count: %v", err)
257 }
258
259 // Small delay to avoid timestamp collisions
260 time.Sleep(10 * time.Millisecond)
261 }
262
263 log.Printf("Created %d top-level comments", len(topLevelComments))
264
265 // Create first-level replies (replies to top-level comments)
266 log.Println("\n=== Creating First-Level Replies ===")
267 firstLevelReplies := make([]*Comment, 0)
268
269 for i, parentComment := range topLevelComments {
270 // 60% chance of having replies
271 if rand.Float64() > 0.6 {
272 continue
273 }
274
275 // 1-3 replies per comment
276 numReplies := 1 + rand.Intn(3)
277 for j := 0; j < numReplies; j++ {
278 userIdx := (i*3 + j + len(topLevelComments)) % len(users)
279 user := users[userIdx]
280 content := replyComments[rand.Intn(len(replyComments))]
281 createdAt := parentComment.CreatedAt.Add(time.Duration(5+rand.Intn(10)) * time.Minute)
282
283 comment, err := createComment(db, user, content, parentComment.URI, parentComment.CID, createdAt)
284 if err != nil {
285 log.Printf("Warning: Failed to create first-level reply: %v", err)
286 continue
287 }
288
289 firstLevelReplies = append(firstLevelReplies, comment)
290
291 // Update parent comment reply count
292 if err := updateCommentCount(db, parentComment.URI, false); err != nil {
293 log.Printf("Warning: Failed to update comment reply count: %v", err)
294 }
295
296 time.Sleep(10 * time.Millisecond)
297 }
298 }
299
300 log.Printf("Created %d first-level replies", len(firstLevelReplies))
301
302 // Create second-level replies (replies to replies) - testing nested threading
303 log.Println("\n=== Creating Second-Level Replies ===")
304 secondLevelCount := 0
305
306 for i, parentComment := range firstLevelReplies {
307 // 40% chance of having deep replies
308 if rand.Float64() > 0.4 {
309 continue
310 }
311
312 // 1-2 deep replies
313 numReplies := 1 + rand.Intn(2)
314 for j := 0; j < numReplies; j++ {
315 userIdx := (i*2 + j + len(topLevelComments) + len(firstLevelReplies)) % len(users)
316 user := users[userIdx]
317 content := deepReplyComments[rand.Intn(len(deepReplyComments))]
318 createdAt := parentComment.CreatedAt.Add(time.Duration(3+rand.Intn(7)) * time.Minute)
319
320 _, err := createComment(db, user, content, parentComment.URI, parentComment.CID, createdAt)
321 if err != nil {
322 log.Printf("Warning: Failed to create second-level reply: %v", err)
323 continue
324 }
325
326 secondLevelCount++
327
328 // Update parent comment reply count
329 if err := updateCommentCount(db, parentComment.URI, false); err != nil {
330 log.Printf("Warning: Failed to update comment reply count: %v", err)
331 }
332
333 time.Sleep(10 * time.Millisecond)
334 }
335 }
336
337 log.Printf("Created %d second-level replies", secondLevelCount)
338
339 // Print summary
340 totalComments := len(topLevelComments) + len(firstLevelReplies) + secondLevelCount
341 log.Println("\n=== Summary ===")
342 log.Printf("Total users created: %d", len(users))
343 log.Printf("Total comments created: %d", totalComments)
344 log.Printf(" - Top-level comments: %d", len(topLevelComments))
345 log.Printf(" - First-level replies: %d", len(firstLevelReplies))
346 log.Printf(" - Second-level replies: %d", secondLevelCount)
347 log.Println("\nDone! Check the post at !test-usnews for the comments.")
348}