1package testing
2
3import (
4 "bytes"
5 "context"
6 "crypto/ecdsa"
7 "crypto/elliptic"
8 "crypto/rand"
9 "encoding/base32"
10 "encoding/json"
11 "fmt"
12 mathrand "math/rand"
13 "net"
14 "net/http"
15 "os"
16 "path/filepath"
17 "strings"
18 "sync"
19 "testing"
20 "time"
21
22 atproto "github.com/bluesky-social/indigo/api/atproto"
23 bsky "github.com/bluesky-social/indigo/api/bsky"
24 "github.com/bluesky-social/indigo/bgs"
25 "github.com/bluesky-social/indigo/carstore"
26 "github.com/bluesky-social/indigo/events"
27 "github.com/bluesky-social/indigo/events/diskpersist"
28 "github.com/bluesky-social/indigo/events/schedulers/sequential"
29 "github.com/bluesky-social/indigo/handles"
30 "github.com/bluesky-social/indigo/indexer"
31 lexutil "github.com/bluesky-social/indigo/lex/util"
32 "github.com/bluesky-social/indigo/models"
33 "github.com/bluesky-social/indigo/pds"
34 "github.com/bluesky-social/indigo/plc"
35 "github.com/bluesky-social/indigo/repo"
36 "github.com/bluesky-social/indigo/repomgr"
37 bsutil "github.com/bluesky-social/indigo/util"
38 "github.com/bluesky-social/indigo/xrpc"
39 "github.com/ipfs/go-cid"
40 "github.com/multiformats/go-multihash"
41 "github.com/whyrusleeping/go-did"
42
43 "net/url"
44
45 "github.com/gorilla/websocket"
46 "gorm.io/driver/sqlite"
47 "gorm.io/gorm"
48)
49
50type TestPDS struct {
51 dir string
52 server *pds.Server
53 plc *plc.PLCServer
54
55 listener net.Listener
56
57 shutdown func()
58}
59
60// RawHost returns a host:port string that the PDS server is running at
61func (tp *TestPDS) RawHost() string {
62 return tp.listener.Addr().String()
63}
64
65// HTTPHost returns a URL string that the PDS server is running at with the
66// scheme set for HTTP
67func (tp *TestPDS) HTTPHost() string {
68 u := url.URL{Scheme: "http", Host: tp.listener.Addr().String()}
69 return u.String()
70}
71
72func (tp *TestPDS) Cleanup() {
73 if tp.shutdown != nil {
74 tp.shutdown()
75 }
76
77 if tp.dir != "" {
78 _ = os.RemoveAll(tp.dir)
79 }
80}
81
82func MustSetupPDS(t *testing.T, suffix string, plc plc.PLCClient) *TestPDS {
83 t.Helper()
84 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
85 defer cancel()
86 tpds, err := SetupPDS(ctx, suffix, plc)
87 if err != nil {
88 t.Fatal(err)
89 }
90
91 return tpds
92}
93
94func SetupPDS(ctx context.Context, suffix string, plc plc.PLCClient) (*TestPDS, error) {
95 dir, err := os.MkdirTemp("", "integtest")
96 if err != nil {
97 return nil, err
98 }
99
100 maindb, err := gorm.Open(sqlite.Open(filepath.Join(dir, "test.sqlite?cache=shared&mode=rwc")))
101 if err != nil {
102 return nil, err
103 }
104
105 tx := maindb.Exec("PRAGMA journal_mode=WAL;")
106 if tx.Error != nil {
107 return nil, tx.Error
108 }
109
110 cardb, err := gorm.Open(sqlite.Open(filepath.Join(dir, "car.sqlite")))
111 if err != nil {
112 return nil, err
113 }
114
115 cspath := filepath.Join(dir, "carstore")
116 if err := os.Mkdir(cspath, 0775); err != nil {
117 return nil, err
118 }
119
120 cs, err := carstore.NewCarStore(cardb, []string{cspath})
121 if err != nil {
122 return nil, err
123 }
124
125 raw, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
126 if err != nil {
127 return nil, fmt.Errorf("failed to generate new ECDSA private key: %s", err)
128 }
129 serkey := &did.PrivKey{
130 Raw: raw,
131 Type: did.KeyTypeP256,
132 }
133
134 var lc net.ListenConfig
135 li, err := lc.Listen(ctx, "tcp", "localhost:0")
136 if err != nil {
137 return nil, err
138 }
139
140 host := li.Addr().String()
141 srv, err := pds.NewServer(maindb, cs, serkey, suffix, host, plc, []byte(host+suffix))
142 if err != nil {
143 return nil, err
144 }
145
146 return &TestPDS{
147 dir: dir,
148 server: srv,
149 listener: li,
150 }, nil
151}
152
153func (tp *TestPDS) Run(t *testing.T) {
154 // TODO: rig this up so it t.Fatals if the RunAPI call fails immediately
155 go func() {
156 if err := tp.server.RunAPIWithListener(tp.listener); err != nil {
157 fmt.Println(err)
158 }
159 }()
160 time.Sleep(time.Millisecond * 10)
161
162 tp.shutdown = func() {
163 tp.server.Shutdown(context.TODO())
164 }
165}
166
167func (tp *TestPDS) RequestScraping(t *testing.T, b *TestRelay) {
168 t.Helper()
169
170 err := b.bgs.CreateAdminToken("test")
171 if err != nil {
172 t.Fatal(err)
173 }
174
175 req, err := http.NewRequest("POST", "http://"+b.Host()+"/admin/subs/setPerDayLimit?limit=500", nil)
176
177 req.Header.Set("Content-Type", "application/json")
178 req.Header.Set("Authorization", "Bearer test")
179
180 // Send the request
181 client := &http.Client{}
182 resp, err := client.Do(req)
183 if err != nil {
184 t.Fatal(err)
185 }
186 defer resp.Body.Close()
187
188 // Check the response
189 if resp.StatusCode != http.StatusOK {
190 t.Fatal("expected 200 OK, got: ", resp.Status)
191 }
192
193 c := &xrpc.Client{Host: "http://" + b.Host()}
194 if err := atproto.SyncRequestCrawl(context.TODO(), c, &atproto.SyncRequestCrawl_Input{Hostname: tp.RawHost()}); err != nil {
195 t.Fatal(err)
196 }
197}
198
199func (tp *TestPDS) BumpLimits(t *testing.T, b *TestRelay) {
200 t.Helper()
201
202 err := b.bgs.CreateAdminToken("test")
203 if err != nil {
204 t.Fatal(err)
205 }
206
207 u, err := url.Parse(tp.HTTPHost())
208 if err != nil {
209 t.Fatal(err)
210 }
211
212 limReqBody := bgs.RateLimitChangeRequest{
213 Host: u.Host,
214 PDSRates: bgs.PDSRates{
215 PerSecond: 5_000,
216 PerHour: 100_000,
217 PerDay: 1_000_000,
218 RepoLimit: 500_000,
219 CrawlRate: 50_000,
220 },
221 }
222
223 // JSON encode the request body
224 reqBody, err := json.Marshal(limReqBody)
225 if err != nil {
226 t.Fatal(err)
227 }
228
229 req, err := http.NewRequest("POST", "http://"+b.Host()+"/admin/pds/changeLimits", bytes.NewBuffer(reqBody))
230 if err != nil {
231 t.Fatal(err)
232 }
233 req.Header.Set("Content-Type", "application/json")
234 req.Header.Set("Authorization", "Bearer test")
235
236 // Send the request
237 client := &http.Client{}
238 resp, err := client.Do(req)
239 if err != nil {
240 t.Fatal(err)
241 }
242 defer resp.Body.Close()
243
244 // Check the response
245 if resp.StatusCode != http.StatusOK {
246 t.Fatal("expected 200 OK, got: ", resp.Status)
247 }
248}
249
250type TestUser struct {
251 handle string
252 pds *TestPDS
253 did string
254
255 client *xrpc.Client
256}
257
258func (tp *TestPDS) MustNewUser(t *testing.T, handle string) *TestUser {
259 t.Helper()
260
261 u, err := tp.NewUser(handle)
262 if err != nil {
263 t.Fatal(err)
264 }
265
266 return u
267}
268
269func (tp *TestPDS) NewUser(handle string) (*TestUser, error) {
270 ctx := context.TODO()
271
272 c := &xrpc.Client{
273 Host: tp.HTTPHost(),
274 }
275
276 fmt.Println("HOST: ", c.Host)
277 email := handle + "@fake.com"
278 pass := "password"
279 out, err := atproto.ServerCreateAccount(ctx, c, &atproto.ServerCreateAccount_Input{
280 Email: &email,
281 Handle: handle,
282 Password: &pass,
283 })
284 if err != nil {
285 return nil, err
286 }
287
288 c.Auth = &xrpc.AuthInfo{
289 AccessJwt: out.AccessJwt,
290 RefreshJwt: out.RefreshJwt,
291 Handle: out.Handle,
292 Did: out.Did,
293 }
294
295 return &TestUser{
296 pds: tp,
297 handle: out.Handle,
298 client: c,
299 did: out.Did,
300 }, nil
301}
302
303func (tp *TestPDS) TakedownRepo(t *testing.T, did string) {
304 req, err := http.NewRequest("GET", tp.HTTPHost()+"/takedownRepo?did="+did, nil)
305 if err != nil {
306 t.Fatal(err)
307 }
308
309 client := &http.Client{}
310 resp, err := client.Do(req)
311 if err != nil {
312 t.Fatal(err)
313 }
314
315 if resp.StatusCode != http.StatusOK {
316 t.Fatal("expected 200 OK, got: ", resp.Status)
317 }
318}
319
320func (tp *TestPDS) SuspendRepo(t *testing.T, did string) {
321 req, err := http.NewRequest("GET", tp.HTTPHost()+"/suspendRepo?did="+did, nil)
322 if err != nil {
323 t.Fatal(err)
324 }
325
326 client := &http.Client{}
327 resp, err := client.Do(req)
328 if err != nil {
329 t.Fatal(err)
330 }
331
332 if resp.StatusCode != http.StatusOK {
333 t.Fatal("expected 200 OK, got: ", resp.Status)
334 }
335}
336
337func (tp *TestPDS) DeactivateRepo(t *testing.T, did string) {
338 req, err := http.NewRequest("GET", tp.HTTPHost()+"/deactivateRepo?did="+did, nil)
339 if err != nil {
340 t.Fatal(err)
341 }
342
343 client := &http.Client{}
344 resp, err := client.Do(req)
345 if err != nil {
346 t.Fatal(err)
347 }
348
349 if resp.StatusCode != http.StatusOK {
350 t.Fatal("expected 200 OK, got: ", resp.Status)
351 }
352}
353
354func (tp *TestPDS) ReactivateRepo(t *testing.T, did string) {
355 req, err := http.NewRequest("GET", tp.HTTPHost()+"/reactivateRepo?did="+did, nil)
356 if err != nil {
357 t.Fatal(err)
358 }
359
360 client := &http.Client{}
361 resp, err := client.Do(req)
362 if err != nil {
363 t.Fatal(err)
364 }
365
366 if resp.StatusCode != http.StatusOK {
367 t.Fatal("expected 200 OK, got: ", resp.Status)
368 }
369}
370
371func (u *TestUser) Reply(t *testing.T, replyto, root *atproto.RepoStrongRef, body string) string {
372 t.Helper()
373
374 ctx := context.TODO()
375 resp, err := atproto.RepoCreateRecord(ctx, u.client, &atproto.RepoCreateRecord_Input{
376 Collection: "app.bsky.feed.post",
377 Repo: u.did,
378 Record: &lexutil.LexiconTypeDecoder{Val: &bsky.FeedPost{
379 CreatedAt: time.Now().Format(time.RFC3339),
380 Text: body,
381 Reply: &bsky.FeedPost_ReplyRef{
382 Parent: replyto,
383 Root: root,
384 }},
385 },
386 })
387 if err != nil {
388 t.Fatal(err)
389 }
390
391 return resp.Uri
392}
393
394func (u *TestUser) DID() string {
395 return u.did
396}
397
398func (u *TestUser) Post(t *testing.T, body string) *atproto.RepoStrongRef {
399 t.Helper()
400
401 ctx := context.TODO()
402 resp, err := atproto.RepoCreateRecord(ctx, u.client, &atproto.RepoCreateRecord_Input{
403 Collection: "app.bsky.feed.post",
404 Repo: u.did,
405 Record: &lexutil.LexiconTypeDecoder{Val: &bsky.FeedPost{
406 CreatedAt: time.Now().Format(time.RFC3339),
407 Text: body,
408 }},
409 })
410
411 if err != nil {
412 t.Fatal(err)
413 }
414
415 return &atproto.RepoStrongRef{
416 Cid: resp.Cid,
417 Uri: resp.Uri,
418 }
419}
420
421func (u *TestUser) Like(t *testing.T, post *atproto.RepoStrongRef) {
422 t.Helper()
423
424 ctx := context.TODO()
425 _, err := atproto.RepoCreateRecord(ctx, u.client, &atproto.RepoCreateRecord_Input{
426 Collection: "app.bsky.feed.like",
427 Repo: u.did,
428 Record: &lexutil.LexiconTypeDecoder{Val: &bsky.FeedLike{
429 LexiconTypeID: "app.bsky.feed.like",
430 CreatedAt: time.Now().Format(time.RFC3339),
431 Subject: post,
432 }},
433 })
434 if err != nil {
435 t.Fatal(err)
436 }
437
438}
439
440func (u *TestUser) Follow(t *testing.T, did string) string {
441 t.Helper()
442
443 ctx := context.TODO()
444 resp, err := atproto.RepoCreateRecord(ctx, u.client, &atproto.RepoCreateRecord_Input{
445 Collection: "app.bsky.graph.follow",
446 Repo: u.did,
447 Record: &lexutil.LexiconTypeDecoder{Val: &bsky.GraphFollow{
448 CreatedAt: time.Now().Format(time.RFC3339),
449 Subject: did,
450 }},
451 })
452
453 if err != nil {
454 t.Fatal(err)
455 }
456
457 return resp.Uri
458}
459
460func (u *TestUser) GetFeed(t *testing.T) []*bsky.FeedDefs_FeedViewPost {
461 t.Helper()
462
463 ctx := context.TODO()
464 resp, err := bsky.FeedGetTimeline(ctx, u.client, "reverse-chronlogical", "", 100)
465 if err != nil {
466 t.Fatal(err)
467 }
468
469 return resp.Feed
470}
471
472func (u *TestUser) ChangeHandle(t *testing.T, nhandle string) {
473 t.Helper()
474
475 ctx := context.TODO()
476 if err := atproto.IdentityUpdateHandle(ctx, u.client, &atproto.IdentityUpdateHandle_Input{
477 Handle: nhandle,
478 }); err != nil {
479 t.Fatal(err)
480 }
481}
482
483func TestPLC(t *testing.T) *plc.FakeDid {
484 // TODO: just do in memory...
485 tdir, err := os.MkdirTemp("", "plcserv")
486 if err != nil {
487 t.Fatal(err)
488 }
489
490 db, err := gorm.Open(sqlite.Open(filepath.Join(tdir, "plc.sqlite")))
491 if err != nil {
492 t.Fatal(err)
493 }
494 return plc.NewFakeDid(db)
495}
496
497type TestRelay struct {
498 bgs *bgs.BGS
499 tr *handles.TestHandleResolver
500 db *gorm.DB
501
502 // listener is owned by by the Relay structure and should be closed by
503 // shutting down the Relay.
504 listener net.Listener
505}
506
507func (t *TestRelay) Host() string {
508 return t.listener.Addr().String()
509}
510
511func MustSetupRelay(t *testing.T, didr plc.PLCClient, archive bool) *TestRelay {
512 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
513 defer cancel()
514 tbgs, err := SetupRelay(ctx, didr, archive)
515 if err != nil {
516 t.Fatal(err)
517 }
518
519 return tbgs
520}
521
522func SetupRelay(ctx context.Context, didr plc.PLCClient, archive bool) (*TestRelay, error) {
523 dir, err := os.MkdirTemp("", "integtest")
524 if err != nil {
525 return nil, err
526 }
527
528 maindb, err := gorm.Open(sqlite.Open(filepath.Join(dir, "test.sqlite")))
529 if err != nil {
530 return nil, err
531 }
532
533 cardb, err := gorm.Open(sqlite.Open(filepath.Join(dir, "car.sqlite")))
534 if err != nil {
535 return nil, err
536 }
537
538 cspath := filepath.Join(dir, "carstore")
539 if err := os.Mkdir(cspath, 0775); err != nil {
540 return nil, err
541 }
542
543 var cs carstore.CarStore
544 if archive {
545 arccs, err := carstore.NewCarStore(cardb, []string{cspath})
546 if err != nil {
547 return nil, err
548 }
549 cs = arccs
550 } else {
551 nacs, err := carstore.NewNonArchivalCarstore(cardb)
552 if err != nil {
553 return nil, err
554 }
555 cs = nacs
556 }
557
558 //kmgr := indexer.NewKeyManager(didr, nil)
559 kmgr := &bsutil.FakeKeyManager{}
560
561 repoman := repomgr.NewRepoManager(cs, kmgr)
562
563 opts := diskpersist.DefaultDiskPersistOptions()
564 opts.EventsPerFile = 10
565 diskpersist, err := diskpersist.NewDiskPersistence(filepath.Join(dir, "dp-primary"), filepath.Join(dir, "dp-archive"), maindb, opts)
566
567 evtman := events.NewEventManager(diskpersist)
568 rf := indexer.NewRepoFetcher(maindb, repoman, 10)
569
570 ix, err := indexer.NewIndexer(maindb, evtman, didr, rf, true)
571 if err != nil {
572 return nil, err
573 }
574
575 repoman.SetEventHandler(func(ctx context.Context, evt *repomgr.RepoEvent) {
576 if err := ix.HandleRepoEvent(ctx, evt); err != nil {
577 fmt.Println("test relay failed to handle repo event", err)
578 }
579 }, true) // TODO: actually want this to be false, but some tests use this to confirm the Relay has seen certain records
580
581 tr := &handles.TestHandleResolver{}
582
583 bgsConfig := bgs.DefaultBGSConfig()
584 bgsConfig.SSL = false
585 b, err := bgs.NewBGS(maindb, ix, repoman, evtman, didr, rf, tr, bgsConfig)
586 if err != nil {
587 return nil, err
588 }
589
590 var lc net.ListenConfig
591 listener, err := lc.Listen(ctx, "tcp", "localhost:0")
592 if err != nil {
593 return nil, err
594 }
595
596 return &TestRelay{
597 db: maindb,
598 bgs: b,
599 tr: tr,
600 listener: listener,
601 }, nil
602}
603
604func (b *TestRelay) Run(t *testing.T) {
605 go func() {
606 if err := b.bgs.StartWithListener(b.listener); err != nil {
607 fmt.Println(err)
608 }
609 }()
610 time.Sleep(time.Millisecond * 10)
611}
612
613func (b *TestRelay) BanDomain(t *testing.T, d string) {
614 t.Helper()
615
616 if err := b.db.Create(&models.DomainBan{
617 Domain: d,
618 }).Error; err != nil {
619 t.Fatal(err)
620 }
621}
622
623type EventStream struct {
624 Lk sync.Mutex
625 Events []*events.XRPCStreamEvent
626 Cancel func()
627
628 Cur int
629}
630
631func (b *TestRelay) Events(t *testing.T, since int64) *EventStream {
632 d := websocket.Dialer{}
633 h := http.Header{}
634
635 q := ""
636 if since >= 0 {
637 q = fmt.Sprintf("?cursor=%d", since)
638 }
639
640 con, resp, err := d.Dial("ws://"+b.Host()+"/xrpc/com.atproto.sync.subscribeRepos"+q, h)
641 if err != nil {
642 t.Fatal(err)
643 }
644
645 if resp.StatusCode != 101 {
646 t.Fatal("expected http 101 response, got: ", resp.StatusCode)
647 }
648
649 ctx, cancel := context.WithCancel(context.Background())
650
651 es := &EventStream{
652 Cancel: cancel,
653 }
654
655 go func() {
656 <-ctx.Done()
657 con.Close()
658 }()
659
660 go func() {
661 rsc := &events.RepoStreamCallbacks{
662 RepoCommit: func(evt *atproto.SyncSubscribeRepos_Commit) error {
663 fmt.Println("received event: ", evt.Seq, evt.Repo, len(es.Events))
664 es.Lk.Lock()
665 es.Events = append(es.Events, &events.XRPCStreamEvent{RepoCommit: evt})
666 es.Lk.Unlock()
667 return nil
668 },
669 RepoSync: func(evt *atproto.SyncSubscribeRepos_Sync) error {
670 fmt.Println("received sync event: ", evt.Seq, evt.Did)
671 es.Lk.Lock()
672 es.Events = append(es.Events, &events.XRPCStreamEvent{RepoSync: evt})
673 es.Lk.Unlock()
674 return nil
675 },
676 RepoIdentity: func(evt *atproto.SyncSubscribeRepos_Identity) error {
677 fmt.Println("received identity event: ", evt.Seq, evt.Did)
678 es.Lk.Lock()
679 es.Events = append(es.Events, &events.XRPCStreamEvent{RepoIdentity: evt})
680 es.Lk.Unlock()
681 return nil
682 },
683 RepoAccount: func(evt *atproto.SyncSubscribeRepos_Account) error {
684 fmt.Println("received account event: ", evt.Seq, evt.Did)
685 es.Lk.Lock()
686 es.Events = append(es.Events, &events.XRPCStreamEvent{RepoAccount: evt})
687 es.Lk.Unlock()
688 return nil
689 },
690 }
691 seqScheduler := sequential.NewScheduler("test", rsc.EventHandler)
692 if err := events.HandleRepoStream(ctx, con, seqScheduler, nil); err != nil {
693 fmt.Println(err)
694 }
695 }()
696
697 return es
698}
699
700func (es *EventStream) Next() *events.XRPCStreamEvent {
701 defer es.Lk.Unlock()
702 for {
703 es.Lk.Lock()
704 if len(es.Events) > es.Cur {
705 es.Cur++
706 return es.Events[es.Cur-1]
707 }
708 es.Lk.Unlock()
709 time.Sleep(time.Millisecond * 10)
710 }
711}
712
713func (es *EventStream) All() []*events.XRPCStreamEvent {
714 es.Lk.Lock()
715 defer es.Lk.Unlock()
716 out := make([]*events.XRPCStreamEvent, len(es.Events))
717 for i, e := range es.Events {
718 out[i] = e
719 }
720
721 return out
722}
723
724func (es *EventStream) WaitFor(n int) []*events.XRPCStreamEvent {
725 var out []*events.XRPCStreamEvent
726 for i := 0; i < n; i++ {
727 out = append(out, es.Next())
728 }
729
730 return out
731}
732
733/*
734func TestBasicFederation(t *testing.T) {
735 assert := assert.New(t)
736 plc := testPLC(t)
737 p1 := setupPDS(t, "0.0.0.0:8812", ".pdsone", plc)
738 p2 := setupPDS(t, "0.0.0.0:8813", ".pdstwo", plc)
739
740 defer p1.Cleanup()
741 defer p2.Cleanup()
742
743 p1.Run(t)
744 p2.Run(t)
745
746 bob := p1.NewUser(t, "bob.pdsone")
747 laura := p2.NewUser(t, "laura.pdstwo")
748
749 p1.PeerWith(t, p2)
750 bob.Follow(t, laura.did)
751
752 bp1 := bob.Post(t, "hello world")
753
754 fmt.Println("LAURA POST!!!!")
755 lp1 := laura.Post(t, "hello bob")
756 time.Sleep(time.Millisecond * 50)
757
758 f := bob.GetFeed(t)
759 assert.Equal(f[0].Post.Uri, bp1.Uri)
760 assert.Equal(f[1].Post.Uri, lp1.Uri)
761
762 lp2 := laura.Post(t, "im posting again!")
763 time.Sleep(time.Millisecond * 50)
764
765 f = bob.GetFeed(t)
766 assert.Equal(f[0].Post.Uri, bp1.Uri)
767 assert.Equal(f[1].Post.Uri, lp1.Uri)
768 assert.Equal(f[2].Post.Uri, lp2.Uri)
769
770 fmt.Println("laura notifications:")
771 lnot := laura.GetNotifs(t)
772 if len(lnot) != 1 {
773 t.Fatal("wrong number of notifications")
774 }
775
776}
777*/
778
779var words = []string{
780 "cat",
781 "is",
782 "cash",
783 "dog",
784 "bad",
785 "system",
786 "random",
787 "skoot",
788 "reply",
789 "fish",
790 "sunshine",
791 "bluesky",
792 "make",
793 "equal",
794 "stars",
795 "water",
796 "parrot",
797}
798
799func MakeRandomPost() string {
800 var out []string
801 for i := 0; i < 20; i++ {
802 out = append(out, words[mathrand.Intn(len(words))])
803 }
804
805 return strings.Join(out, " ")
806}
807
808var usernames = []string{
809 "alice",
810 "bob",
811 "carol",
812 "darin",
813 "eve",
814 "francis",
815 "gerald",
816 "hank",
817 "ian",
818 "jeremy",
819 "karl",
820 "louise",
821 "marion",
822 "nancy",
823 "oscar",
824 "paul",
825 "quentin",
826 "raul",
827 "serena",
828 "trevor",
829 "ursula",
830 "valerie",
831 "walter",
832 "xico",
833 "yousef",
834 "zane",
835}
836
837func RandSentence(words []string, maxl int) string {
838 var out string
839 for {
840 w := words[mathrand.Intn(len(words))]
841 if len(out)+len(w) >= maxl {
842 return out
843 }
844
845 out = out + " " + w
846 }
847}
848
849func ReadWords() ([]string, error) {
850 b, err := os.ReadFile("/usr/share/dict/words")
851 if err != nil {
852 return nil, err
853 }
854
855 return strings.Split(string(b), "\n"), nil
856}
857
858func RandFakeCid() cid.Cid {
859 buf := make([]byte, 32)
860 rand.Read(buf)
861
862 pref := cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256)
863 c, err := pref.Sum(buf)
864 if err != nil {
865 panic(err)
866 }
867
868 return c
869}
870
871func RandFakeAtUri(collection, rkey string) string {
872 buf := make([]byte, 10)
873 rand.Read(buf)
874 did := base32.StdEncoding.EncodeToString(buf)
875
876 if rkey == "" {
877 rand.Read(buf)
878 rkey = base32.StdEncoding.EncodeToString(buf[:6])
879 }
880
881 return fmt.Sprintf("at://did:plc:%s/%s/%s", did, collection, rkey)
882}
883
884func RandAction() string {
885 v := mathrand.Intn(100)
886 if v < 40 {
887 return "post"
888 } else if v < 60 {
889 return "repost"
890 } else if v < 80 {
891 return "reply"
892 } else {
893 return "like"
894 }
895}
896
897func GenerateFakeRepo(r *repo.Repo, size int) (cid.Cid, error) {
898 words, err := ReadWords()
899 if err != nil {
900 return cid.Undef, err
901 }
902
903 ctx := context.TODO()
904
905 var root cid.Cid
906 for i := 0; i < size; i++ {
907 switch RandAction() {
908 case "post":
909 _, _, err := r.CreateRecord(ctx, "app.bsky.feed.post", &bsky.FeedPost{
910 CreatedAt: time.Now().Format(bsutil.ISO8601),
911 Text: RandSentence(words, 200),
912 })
913 if err != nil {
914 return cid.Undef, err
915 }
916 case "repost":
917 _, _, err := r.CreateRecord(ctx, "app.bsky.feed.repost", &bsky.FeedRepost{
918 CreatedAt: time.Now().Format(bsutil.ISO8601),
919 Subject: &atproto.RepoStrongRef{
920 Uri: RandFakeAtUri("app.bsky.feed.post", ""),
921 Cid: RandFakeCid().String(),
922 },
923 })
924 if err != nil {
925 return cid.Undef, err
926 }
927 case "reply":
928 _, _, err := r.CreateRecord(ctx, "app.bsky.feed.post", &bsky.FeedPost{
929 CreatedAt: time.Now().Format(bsutil.ISO8601),
930 Text: RandSentence(words, 200),
931 Reply: &bsky.FeedPost_ReplyRef{
932 Root: &atproto.RepoStrongRef{
933 Uri: RandFakeAtUri("app.bsky.feed.post", ""),
934 Cid: RandFakeCid().String(),
935 },
936 Parent: &atproto.RepoStrongRef{
937 Uri: RandFakeAtUri("app.bsky.feed.post", ""),
938 Cid: RandFakeCid().String(),
939 },
940 },
941 })
942 if err != nil {
943 return cid.Undef, err
944 }
945 case "like":
946 _, _, err := r.CreateRecord(ctx, "app.bsky.feed.like", &bsky.FeedLike{
947 CreatedAt: time.Now().Format(bsutil.ISO8601),
948 Subject: &atproto.RepoStrongRef{
949 Uri: RandFakeAtUri("app.bsky.feed.post", ""),
950 Cid: RandFakeCid().String(),
951 },
952 })
953 if err != nil {
954 return cid.Undef, err
955 }
956 }
957
958 kmgr := &bsutil.FakeKeyManager{}
959
960 nroot, _, err := r.Commit(ctx, kmgr.SignForUser)
961 if err != nil {
962 return cid.Undef, err
963 }
964
965 root = nroot
966 }
967
968 return root, nil
969}