this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fakermaker: refactor core functions into package

+588 -541
+36 -541
cmd/fakermaker/main.go
··· 5 5 package main 6 6 7 7 import ( 8 - "bytes" 9 - "context" 10 8 "encoding/json" 11 9 "fmt" 12 - "math/rand" 13 10 "os" 14 11 "runtime" 15 - "time" 16 12 17 - comatproto "github.com/bluesky-social/indigo/api/atproto" 18 - appbsky "github.com/bluesky-social/indigo/api/bsky" 19 13 cliutil "github.com/bluesky-social/indigo/cmd/gosky/util" 20 - lexutil "github.com/bluesky-social/indigo/lex/util" 21 - "github.com/bluesky-social/indigo/util" 14 + "github.com/bluesky-social/indigo/fakedata" 22 15 "github.com/bluesky-social/indigo/version" 23 - "github.com/bluesky-social/indigo/xrpc" 24 16 25 17 _ "github.com/joho/godotenv/autoload" 26 18 27 - "github.com/brianvoe/gofakeit/v6" 28 - logging "github.com/ipfs/go-log" 29 19 "github.com/urfave/cli/v2" 30 20 "golang.org/x/sync/errgroup" 31 21 ) 32 - 33 - var log = logging.Logger("fakermaker") 34 22 35 23 func main() { 36 24 run(os.Args) ··· 39 27 func run(args []string) { 40 28 41 29 app := cli.App{ 42 - Name: "fakermaker", 43 - Usage: "bluesky fake account/content generator", 30 + Name: "fakermaker", 31 + Usage: "bluesky fake account/content generator", 32 + Version: version.Version, 44 33 } 45 34 46 35 app.Flags = []cli.Flag{ ··· 193 182 }, 194 183 }, 195 184 } 196 - all := measureIterations("entire command") 185 + all := fakedata.MeasureIterations("entire command") 197 186 app.RunAndExitOnError() 198 187 all(1) 199 188 } 200 189 201 - type AccountContext struct { 202 - // 0-based index; should match index 203 - Index int `json:"index"` 204 - AccountType string `json:"accountType"` 205 - Email string `json:"email"` 206 - Password string `json:"password"` 207 - Auth xrpc.AuthInfo `json:"auth"` 208 - } 209 - 210 - func accountXrpcClient(cctx *cli.Context, ac *AccountContext) (*xrpc.Client, error) { 211 - pdsHost := cctx.String("pds-host") 212 - httpClient := util.RobustHTTPClient() 213 - ua := "IndigoFakerMaker/" + version.Version 214 - xrpcc := &xrpc.Client{ 215 - Client: httpClient, 216 - Host: pdsHost, 217 - Auth: &ac.Auth, 218 - UserAgent: &ua, 219 - } 220 - // use XRPC client to re-auth using user/pass 221 - auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ 222 - Identifier: &ac.Auth.Handle, 223 - Password: ac.Password, 224 - }) 225 - if err != nil { 226 - return nil, err 227 - } 228 - xrpcc.Auth.AccessJwt = auth.AccessJwt 229 - xrpcc.Auth.RefreshJwt = auth.RefreshJwt 230 - return xrpcc, nil 231 - } 232 - 233 - type AccountCatalog struct { 234 - Celebs []AccountContext 235 - Regulars []AccountContext 236 - } 237 - 238 190 // registers fake accounts with PDS, and spits out JSON-lines to stdout with auth info 239 191 func genAccounts(cctx *cli.Context) error { 240 192 ··· 256 208 countRegulars := countTotal - countCelebrities 257 209 258 210 // call helper to do actual creation 259 - var usr *AccountContext 211 + var usr *fakedata.AccountContext 260 212 var line []byte 261 - t1 := measureIterations("register celebrity accounts") 213 + t1 := fakedata.MeasureIterations("register celebrity accounts") 262 214 for i := 0; i < countCelebrities; i++ { 263 - if usr, err = pdsGenAccount(xrpcc, i, "celebrity"); err != nil { 215 + if usr, err = fakedata.GenAccount(xrpcc, i, "celebrity"); err != nil { 264 216 return err 265 217 } 266 218 // compact single-line JSON by default ··· 271 223 } 272 224 t1(countCelebrities) 273 225 274 - t2 := measureIterations("register regular accounts") 226 + t2 := fakedata.MeasureIterations("register regular accounts") 275 227 for i := 0; i < countRegulars; i++ { 276 - if usr, err = pdsGenAccount(xrpcc, i, "regular"); err != nil { 228 + if usr, err = fakedata.GenAccount(xrpcc, i, "regular"); err != nil { 277 229 return err 278 230 } 279 231 // compact single-line JSON by default ··· 286 238 return nil 287 239 } 288 240 289 - func measureIterations(name string) func(int) { 290 - start := time.Now() 291 - return func(count int) { 292 - if count == 0 { 293 - return 294 - } 295 - total := time.Since(start) 296 - log.Infof("%s wall runtime: count=%d total=%s mean=%s", name, count, total, total/time.Duration(count)) 297 - } 298 - } 299 - func pdsGenAccount(xrpcc *xrpc.Client, index int, accountType string) (*AccountContext, error) { 300 - var suffix string 301 - if accountType == "celebrity" { 302 - suffix = "C" 303 - } else { 304 - suffix = "" 305 - } 306 - prefix := gofakeit.Username() 307 - if len(prefix) > 10 { 308 - prefix = prefix[0:10] 309 - } 310 - handle := fmt.Sprintf("%s-%s%d.test", prefix, suffix, index) 311 - email := gofakeit.Email() 312 - password := gofakeit.Password(true, true, true, true, true, 24) 313 - ctx := context.TODO() 314 - resp, err := comatproto.ServerCreateAccount(ctx, xrpcc, &comatproto.ServerCreateAccount_Input{ 315 - Email: email, 316 - Handle: handle, 317 - Password: password, 318 - }) 319 - if err != nil { 320 - return nil, err 321 - } 322 - auth := xrpc.AuthInfo{ 323 - AccessJwt: resp.AccessJwt, 324 - RefreshJwt: resp.RefreshJwt, 325 - Handle: resp.Handle, 326 - Did: resp.Did, 327 - } 328 - return &AccountContext{ 329 - Index: index, 330 - AccountType: accountType, 331 - Email: email, 332 - Password: password, 333 - Auth: auth, 334 - }, nil 335 - } 336 - 337 - func readAccountCatalog(path string) (*AccountCatalog, error) { 338 - catalog := &AccountCatalog{} 339 - catFile, err := os.Open(path) 340 - if err != nil { 341 - return nil, err 342 - } 343 - defer catFile.Close() 344 - 345 - decoder := json.NewDecoder(catFile) 346 - for decoder.More() { 347 - var usr AccountContext 348 - if err := decoder.Decode(&usr); err != nil { 349 - return nil, fmt.Errorf("parse AccountContext: %w", err) 350 - } 351 - if usr.AccountType == "celebrity" { 352 - catalog.Celebs = append(catalog.Celebs, usr) 353 - } else { 354 - catalog.Regulars = append(catalog.Regulars, usr) 355 - } 356 - } 357 - // validate index numbers 358 - for i, u := range catalog.Celebs { 359 - if i != u.Index { 360 - return nil, fmt.Errorf("account index didn't match: %d != %d (%s)", i, u.Index, u.AccountType) 361 - } 362 - } 363 - for i, u := range catalog.Regulars { 364 - if i != u.Index { 365 - return nil, fmt.Errorf("account index didn't match: %d != %d (%s)", i, u.Index, u.AccountType) 366 - } 367 - } 368 - log.Infof("loaded account catalog: regular=%d celebrity=%d", len(catalog.Regulars), len(catalog.Celebs)) 369 - return catalog, nil 370 - } 371 - 372 241 func genProfiles(cctx *cli.Context) error { 373 - catalog, err := readAccountCatalog(cctx.String("catalog")) 242 + catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 374 243 if err != nil { 375 244 return err 376 245 } 377 246 247 + pdsHost := cctx.String("pds-host") 378 248 genAvatar := !cctx.Bool("no-avatars") 379 249 genBanner := !cctx.Bool("no-banners") 380 250 jobs := cctx.Int("jobs") 381 251 382 - accChan := make(chan AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 252 + accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 383 253 eg := new(errgroup.Group) 384 254 for i := 0; i < jobs; i++ { 385 255 eg.Go(func() error { 386 256 for acc := range accChan { 387 - xrpcc, err := accountXrpcClient(cctx, &acc) 257 + xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 388 258 if err != nil { 389 259 return err 390 260 } 391 - if err = pdsGenProfile(xrpcc, &acc, genAvatar, genBanner); err != nil { 261 + if err = fakedata.GenProfile(xrpcc, &acc, genAvatar, genBanner); err != nil { 392 262 return err 393 263 } 394 264 } ··· 403 273 return eg.Wait() 404 274 } 405 275 406 - func pdsGenProfile(xrpcc *xrpc.Client, acc *AccountContext, genAvatar, genBanner bool) error { 407 - 408 - desc := gofakeit.HipsterSentence(12) 409 - var name string 410 - if acc.AccountType == "celebrity" { 411 - name = gofakeit.CelebrityActor() 412 - } else { 413 - name = gofakeit.Name() 414 - } 415 - 416 - var avatar *lexutil.LexBlob 417 - if genAvatar { 418 - img := gofakeit.ImagePng(200, 200) 419 - resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 420 - if err != nil { 421 - return err 422 - } 423 - avatar = &lexutil.LexBlob{ 424 - Ref: resp.Blob.Ref, 425 - MimeType: "image/png", 426 - Size: resp.Blob.Size, 427 - } 428 - } 429 - var banner *lexutil.LexBlob 430 - if genBanner { 431 - img := gofakeit.ImageJpeg(800, 200) 432 - resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 433 - if err != nil { 434 - return err 435 - } 436 - banner = &lexutil.LexBlob{ 437 - Ref: resp.Blob.Ref, 438 - MimeType: "image/jpeg", 439 - Size: resp.Blob.Size, 440 - } 441 - } 442 - 443 - _, err := comatproto.RepoPutRecord(context.TODO(), xrpcc, &comatproto.RepoPutRecord_Input{ 444 - Repo: acc.Auth.Did, 445 - Collection: "app.bsky.actor.profile", 446 - Rkey: "self", 447 - Record: &lexutil.LexiconTypeDecoder{&appbsky.ActorProfile{ 448 - Description: &desc, 449 - DisplayName: &name, 450 - Avatar: avatar, 451 - Banner: banner, 452 - }}, 453 - }) 454 - return err 455 - } 456 - 457 276 func genGraph(cctx *cli.Context) error { 458 - catalog, err := readAccountCatalog(cctx.String("catalog")) 277 + catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 459 278 if err != nil { 460 279 return err 461 280 } 462 281 282 + pdsHost := cctx.String("pds-host") 463 283 maxFollows := cctx.Int("max-follows") 464 284 maxMutes := cctx.Int("max-mutes") 465 285 jobs := cctx.Int("jobs") 466 286 467 - accChan := make(chan AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 287 + accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 468 288 eg := new(errgroup.Group) 469 289 for i := 0; i < jobs; i++ { 470 290 eg.Go(func() error { 471 291 for acc := range accChan { 472 - xrpcc, err := accountXrpcClient(cctx, &acc) 292 + xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 473 293 if err != nil { 474 294 return err 475 295 } 476 - if err = pdsGenFollowsAndMutes(xrpcc, catalog, &acc, maxFollows, maxMutes); err != nil { 296 + if err = fakedata.GenFollowsAndMutes(xrpcc, catalog, &acc, maxFollows, maxMutes); err != nil { 477 297 return err 478 298 } 479 299 } ··· 488 308 return eg.Wait() 489 309 } 490 310 491 - func pdsGenPosts(xrpcc *xrpc.Client, catalog *AccountCatalog, acc *AccountContext, maxPosts int, fracImage float64, fracMention float64) error { 492 - 493 - var mention *appbsky.FeedPost_Entity 494 - var tgt *AccountContext 495 - var text string 496 - ctx := context.TODO() 497 - 498 - if maxPosts < 1 { 499 - return nil 500 - } 501 - count := rand.Intn(maxPosts) 502 - 503 - // celebrities make 2x the posts 504 - if acc.AccountType == "celebrity" { 505 - count = count * 2 506 - } 507 - t1 := measureIterations("generate posts") 508 - for i := 0; i < count; i++ { 509 - text = gofakeit.Sentence(10) 510 - if len(text) > 200 { 511 - text = text[0:200] 512 - } 513 - 514 - // half the time, mention a celeb 515 - tgt = nil 516 - mention = nil 517 - if fracMention > 0.0 && rand.Float64() < fracMention/2 { 518 - tgt = &catalog.Regulars[rand.Intn(len(catalog.Regulars))] 519 - } else if fracMention > 0.0 && rand.Float64() < fracMention/2 { 520 - tgt = &catalog.Celebs[rand.Intn(len(catalog.Celebs))] 521 - } 522 - if tgt != nil { 523 - text = "@" + tgt.Auth.Handle + " " + text 524 - mention = &appbsky.FeedPost_Entity{ 525 - Type: "mention", 526 - Value: tgt.Auth.Did, 527 - Index: &appbsky.FeedPost_TextSlice{ 528 - Start: 0, 529 - End: int64(len(tgt.Auth.Handle) + 1), 530 - }, 531 - } 532 - } 533 - 534 - var images []*appbsky.EmbedImages_Image 535 - if fracImage > 0.0 && rand.Float64() < fracImage { 536 - img := gofakeit.ImageJpeg(800, 800) 537 - resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 538 - if err != nil { 539 - return err 540 - } 541 - images = append(images, &appbsky.EmbedImages_Image{ 542 - Alt: gofakeit.Lunch(), 543 - Image: &lexutil.LexBlob{ 544 - Ref: resp.Blob.Ref, 545 - MimeType: "image/jpeg", 546 - Size: resp.Blob.Size, 547 - }, 548 - }) 549 - } 550 - post := appbsky.FeedPost{ 551 - Text: text, 552 - CreatedAt: time.Now().Format(time.RFC3339), 553 - } 554 - if mention != nil { 555 - post.Entities = []*appbsky.FeedPost_Entity{mention} 556 - } 557 - if len(images) > 0 { 558 - post.Embed = &appbsky.FeedPost_Embed{ 559 - EmbedImages: &appbsky.EmbedImages{ 560 - Images: images, 561 - }, 562 - } 563 - } 564 - if _, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{ 565 - Collection: "app.bsky.feed.post", 566 - Repo: acc.Auth.Did, 567 - Record: &lexutil.LexiconTypeDecoder{&post}, 568 - }); err != nil { 569 - return err 570 - } 571 - } 572 - t1(count) 573 - return nil 574 - } 575 - 576 - func pdsCreateFollow(xrpcc *xrpc.Client, tgt *AccountContext) error { 577 - follow := &appbsky.GraphFollow{ 578 - CreatedAt: time.Now().Format(time.RFC3339), 579 - Subject: tgt.Auth.Did, 580 - } 581 - _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 582 - Collection: "app.bsky.graph.follow", 583 - Repo: xrpcc.Auth.Did, 584 - Record: &lexutil.LexiconTypeDecoder{follow}, 585 - }) 586 - return err 587 - } 588 - 589 - func pdsCreateLike(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 590 - ctx := context.TODO() 591 - like := appbsky.FeedLike{ 592 - Subject: &comatproto.RepoStrongRef{ 593 - Uri: viewPost.Post.Uri, 594 - Cid: viewPost.Post.Cid, 595 - }, 596 - CreatedAt: time.Now().Format(time.RFC3339), 597 - } 598 - // TODO: may have already like? in that case should ignore error 599 - _, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{ 600 - Collection: "app.bsky.feed.like", 601 - Repo: xrpcc.Auth.Did, 602 - Record: &lexutil.LexiconTypeDecoder{&like}, 603 - }) 604 - return err 605 - } 606 - 607 - func pdsCreateRepost(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 608 - repost := &appbsky.FeedRepost{ 609 - CreatedAt: time.Now().Format(time.RFC3339), 610 - Subject: &comatproto.RepoStrongRef{ 611 - Uri: viewPost.Post.Uri, 612 - Cid: viewPost.Post.Cid, 613 - }, 614 - } 615 - _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 616 - Collection: "app.bsky.feed.repost", 617 - Repo: xrpcc.Auth.Did, 618 - Record: &lexutil.LexiconTypeDecoder{repost}, 619 - }) 620 - return err 621 - } 622 - 623 - func pdsCreateReply(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 624 - text := gofakeit.Sentence(10) 625 - if len(text) > 200 { 626 - text = text[0:200] 627 - } 628 - parent := &comatproto.RepoStrongRef{ 629 - Uri: viewPost.Post.Uri, 630 - Cid: viewPost.Post.Cid, 631 - } 632 - root := parent 633 - if viewPost.Reply != nil { 634 - root = &comatproto.RepoStrongRef{ 635 - Uri: viewPost.Reply.Root.Uri, 636 - Cid: viewPost.Reply.Root.Cid, 637 - } 638 - } 639 - replyPost := &appbsky.FeedPost{ 640 - CreatedAt: time.Now().Format(time.RFC3339), 641 - Text: text, 642 - Reply: &appbsky.FeedPost_ReplyRef{ 643 - Parent: parent, 644 - Root: root, 645 - }, 646 - } 647 - _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 648 - Collection: "app.bsky.feed.post", 649 - Repo: xrpcc.Auth.Did, 650 - Record: &lexutil.LexiconTypeDecoder{replyPost}, 651 - }) 652 - return err 653 - } 654 - 655 - func pdsGenFollowsAndMutes(xrpcc *xrpc.Client, catalog *AccountCatalog, acc *AccountContext, maxFollows int, maxMutes int) error { 656 - 657 - // TODO: have a "shape" to likelihood of doing a follow 658 - var tgt *AccountContext 659 - 660 - if maxFollows > len(catalog.Regulars) { 661 - return fmt.Errorf("not enought regulars to pick maxFollowers from") 662 - } 663 - if maxMutes > len(catalog.Regulars) { 664 - return fmt.Errorf("not enought regulars to pick maxMutes from") 665 - } 666 - 667 - regCount := 0 668 - celebCount := 0 669 - if maxFollows >= 1 { 670 - regCount = rand.Intn(maxFollows) 671 - celebCount = rand.Intn(len(catalog.Celebs)) 672 - } 673 - t1 := measureIterations("generate follows") 674 - for idx := range rand.Perm(len(catalog.Celebs))[:celebCount] { 675 - tgt = &catalog.Celebs[idx] 676 - if tgt.Auth.Did == acc.Auth.Did { 677 - continue 678 - } 679 - if err := pdsCreateFollow(xrpcc, tgt); err != nil { 680 - return err 681 - } 682 - } 683 - for idx := range rand.Perm(len(catalog.Regulars))[:regCount] { 684 - tgt = &catalog.Regulars[idx] 685 - if tgt.Auth.Did == acc.Auth.Did { 686 - continue 687 - } 688 - if err := pdsCreateFollow(xrpcc, tgt); err != nil { 689 - return err 690 - } 691 - } 692 - t1(regCount + celebCount) 693 - 694 - // only muting other users, not celebs 695 - muteCount := 0 696 - if maxFollows >= 1 { 697 - muteCount = rand.Intn(maxMutes) 698 - } 699 - t2 := measureIterations("generate mutes") 700 - for idx := range rand.Perm(len(catalog.Regulars))[:muteCount] { 701 - tgt = &catalog.Regulars[idx] 702 - if tgt.Auth.Did == acc.Auth.Did { 703 - continue 704 - } 705 - if err := appbsky.GraphMuteActor(context.TODO(), xrpcc, &appbsky.GraphMuteActor_Input{Actor: tgt.Auth.Did}); err != nil { 706 - return err 707 - } 708 - } 709 - t2(muteCount) 710 - return nil 711 - } 712 - 713 311 func genPosts(cctx *cli.Context) error { 714 - catalog, err := readAccountCatalog(cctx.String("catalog")) 312 + catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 715 313 if err != nil { 716 314 return err 717 315 } 718 316 317 + pdsHost := cctx.String("pds-host") 719 318 maxPosts := cctx.Int("max-posts") 720 319 fracImage := cctx.Float64("frac-image") 721 320 fracMention := cctx.Float64("frac-mention") 722 321 jobs := cctx.Int("jobs") 723 322 724 - accChan := make(chan AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 323 + accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 725 324 eg := new(errgroup.Group) 726 325 for i := 0; i < jobs; i++ { 727 326 eg.Go(func() error { 728 327 for acc := range accChan { 729 - xrpcc, err := accountXrpcClient(cctx, &acc) 328 + xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 730 329 if err != nil { 731 330 return err 732 331 } 733 - if err = pdsGenPosts(xrpcc, catalog, &acc, maxPosts, fracImage, fracMention); err != nil { 332 + if err = fakedata.GenPosts(xrpcc, catalog, &acc, maxPosts, fracImage, fracMention); err != nil { 734 333 return err 735 334 } 736 335 } ··· 746 345 } 747 346 748 347 func genInteractions(cctx *cli.Context) error { 749 - catalog, err := readAccountCatalog(cctx.String("catalog")) 348 + catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 750 349 if err != nil { 751 350 return err 752 351 } 753 352 353 + pdsHost := cctx.String("pds-host") 754 354 fracLike := cctx.Float64("frac-like") 755 355 fracRepost := cctx.Float64("frac-repost") 756 356 fracReply := cctx.Float64("frac-reply") 757 357 jobs := cctx.Int("jobs") 758 358 759 - accChan := make(chan AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 359 + accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 760 360 eg := new(errgroup.Group) 761 361 for i := 0; i < jobs; i++ { 762 362 eg.Go(func() error { 763 363 for acc := range accChan { 764 - xrpcc, err := accountXrpcClient(cctx, &acc) 364 + xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 765 365 if err != nil { 766 366 return err 767 367 } 768 - t1 := measureIterations("all interactions") 769 - // fetch timeline (up to 100), and iterate over posts 770 - maxTimeline := 100 771 - resp, err := appbsky.FeedGetTimeline(context.TODO(), xrpcc, "", "", int64(maxTimeline)) 772 - if err != nil { 368 + t1 := fakedata.MeasureIterations("all interactions") 369 + if err := fakedata.GenLikesRepostsReplies(xrpcc, &acc, fracLike, fracRepost, fracReply); err != nil { 773 370 return err 774 371 } 775 - if len(resp.Feed) > maxTimeline { 776 - return fmt.Errorf("got too long timeline len=%d", len(resp.Feed)) 777 - } 778 - for _, post := range resp.Feed { 779 - // skip account's own posts 780 - if post.Post.Author.Did == acc.Auth.Did { 781 - continue 782 - } 783 - 784 - // generate 785 - if fracLike > 0.0 && rand.Float64() < fracLike { 786 - if err := pdsCreateLike(xrpcc, post); err != nil { 787 - return err 788 - } 789 - } 790 - if fracRepost > 0.0 && rand.Float64() < fracRepost { 791 - if err := pdsCreateRepost(xrpcc, post); err != nil { 792 - return err 793 - } 794 - } 795 - if fracReply > 0.0 && rand.Float64() < fracReply { 796 - if err := pdsCreateReply(xrpcc, post); err != nil { 797 - return err 798 - } 799 - } 800 - } 801 372 t1(1) 802 373 } 803 374 return nil ··· 811 382 return eg.Wait() 812 383 } 813 384 814 - func browseAccount(xrpcc *xrpc.Client, acc *AccountContext) error { 815 - // fetch notifications 816 - maxNotif := 50 817 - resp, err := appbsky.NotificationListNotifications(context.TODO(), xrpcc, "", int64(maxNotif)) 818 - if err != nil { 819 - return err 820 - } 821 - if len(resp.Notifications) > maxNotif { 822 - return fmt.Errorf("got too many notifications len=%d", len(resp.Notifications)) 823 - } 824 - t1 := measureIterations("notification interactions") 825 - for _, notif := range resp.Notifications { 826 - switch notif.Reason { 827 - case "vote": 828 - fallthrough 829 - case "repost": 830 - fallthrough 831 - case "follow": 832 - _, err := appbsky.ActorGetProfile(context.TODO(), xrpcc, notif.Author.Did) 833 - if err != nil { 834 - return err 835 - } 836 - _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, notif.Author.Did, "", 50) 837 - if err != nil { 838 - return err 839 - } 840 - case "mention": 841 - fallthrough 842 - case "reply": 843 - _, err := appbsky.FeedGetPostThread(context.TODO(), xrpcc, 4, notif.Uri) 844 - if err != nil { 845 - return err 846 - } 847 - default: 848 - } 849 - } 850 - t1(len(resp.Notifications)) 851 - 852 - // fetch timeline (up to 100), and iterate over posts 853 - timelineLen := 100 854 - timelineResp, err := appbsky.FeedGetTimeline(context.TODO(), xrpcc, "", "", int64(timelineLen)) 855 - if err != nil { 856 - return err 857 - } 858 - if len(timelineResp.Feed) > timelineLen { 859 - return fmt.Errorf("longer than expected timeline len=%d", len(timelineResp.Feed)) 860 - } 861 - t2 := measureIterations("timeline interactions") 862 - for _, post := range timelineResp.Feed { 863 - // skip account's own posts 864 - if post.Post.Author.Did == acc.Auth.Did { 865 - continue 866 - } 867 - // TODO: should we do something different here? 868 - if rand.Float64() < 0.25 { 869 - _, err = appbsky.FeedGetPostThread(context.TODO(), xrpcc, 4, post.Post.Uri) 870 - if err != nil { 871 - return err 872 - } 873 - } else if rand.Float64() < 0.25 { 874 - _, err = appbsky.ActorGetProfile(context.TODO(), xrpcc, post.Post.Author.Did) 875 - if err != nil { 876 - return err 877 - } 878 - _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, post.Post.Author.Did, "", 50) 879 - if err != nil { 880 - return err 881 - } 882 - } 883 - } 884 - t2(len(timelineResp.Feed)) 885 - 886 - // notification count for good measure 887 - _, err = appbsky.NotificationGetUnreadCount(context.TODO(), xrpcc) 888 - return err 889 - } 890 - 891 385 func runBrowsing(cctx *cli.Context) error { 892 - catalog, err := readAccountCatalog(cctx.String("catalog")) 386 + catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 893 387 if err != nil { 894 388 return err 895 389 } 896 390 391 + pdsHost := cctx.String("pds-host") 897 392 jobs := cctx.Int("jobs") 898 393 899 - accChan := make(chan AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 394 + accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 900 395 eg := new(errgroup.Group) 901 396 for i := 0; i < jobs; i++ { 902 397 eg.Go(func() error { 903 398 for acc := range accChan { 904 - xrpcc, err := accountXrpcClient(cctx, &acc) 399 + xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 905 400 if err != nil { 906 401 return err 907 402 } 908 - if err := browseAccount(xrpcc, &acc); err != nil { 403 + if err := fakedata.BrowseAccount(xrpcc, &acc); err != nil { 909 404 return err 910 405 } 911 406 }
+95
fakedata/accounts.go
··· 1 + package fakedata 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "os" 8 + 9 + comatproto "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/util" 11 + "github.com/bluesky-social/indigo/version" 12 + "github.com/bluesky-social/indigo/xrpc" 13 + ) 14 + 15 + type AccountCatalog struct { 16 + Celebs []AccountContext 17 + Regulars []AccountContext 18 + } 19 + 20 + func (ac *AccountCatalog) Combined() []AccountContext { 21 + var combined []AccountContext 22 + for _, c := range ac.Celebs { 23 + combined = append(combined, c) 24 + } 25 + for _, r := range ac.Regulars { 26 + combined = append(combined, r) 27 + } 28 + return combined 29 + } 30 + 31 + type AccountContext struct { 32 + // 0-based index; should match index 33 + Index int `json:"index"` 34 + AccountType string `json:"accountType"` 35 + Email string `json:"email"` 36 + Password string `json:"password"` 37 + Auth xrpc.AuthInfo `json:"auth"` 38 + } 39 + 40 + func ReadAccountCatalog(path string) (*AccountCatalog, error) { 41 + catalog := &AccountCatalog{} 42 + catFile, err := os.Open(path) 43 + if err != nil { 44 + return nil, err 45 + } 46 + defer catFile.Close() 47 + 48 + decoder := json.NewDecoder(catFile) 49 + for decoder.More() { 50 + var usr AccountContext 51 + if err := decoder.Decode(&usr); err != nil { 52 + return nil, fmt.Errorf("parse AccountContext: %w", err) 53 + } 54 + if usr.AccountType == "celebrity" { 55 + catalog.Celebs = append(catalog.Celebs, usr) 56 + } else { 57 + catalog.Regulars = append(catalog.Regulars, usr) 58 + } 59 + } 60 + // validate index numbers 61 + for i, u := range catalog.Celebs { 62 + if i != u.Index { 63 + return nil, fmt.Errorf("account index didn't match: %d != %d (%s)", i, u.Index, u.AccountType) 64 + } 65 + } 66 + for i, u := range catalog.Regulars { 67 + if i != u.Index { 68 + return nil, fmt.Errorf("account index didn't match: %d != %d (%s)", i, u.Index, u.AccountType) 69 + } 70 + } 71 + log.Infof("loaded account catalog: regular=%d celebrity=%d", len(catalog.Regulars), len(catalog.Celebs)) 72 + return catalog, nil 73 + } 74 + 75 + func AccountXrpcClient(pdsHost string, ac *AccountContext) (*xrpc.Client, error) { 76 + httpClient := util.RobustHTTPClient() 77 + ua := "IndigoFakerMaker/" + version.Version 78 + xrpcc := &xrpc.Client{ 79 + Client: httpClient, 80 + Host: pdsHost, 81 + Auth: &ac.Auth, 82 + UserAgent: &ua, 83 + } 84 + // use XRPC client to re-auth using user/pass 85 + auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ 86 + Identifier: &ac.Auth.Handle, 87 + Password: ac.Password, 88 + }) 89 + if err != nil { 90 + return nil, err 91 + } 92 + xrpcc.Auth.AccessJwt = auth.AccessJwt 93 + xrpcc.Auth.RefreshJwt = auth.RefreshJwt 94 + return xrpcc, nil 95 + }
+457
fakedata/generators.go
··· 1 + // Helpers for randomly generated app.bsky.* content: accounts, posts, likes, 2 + // follows, mentions, etc 3 + 4 + package fakedata 5 + 6 + import ( 7 + "bytes" 8 + "context" 9 + "fmt" 10 + "math/rand" 11 + "time" 12 + 13 + comatproto "github.com/bluesky-social/indigo/api/atproto" 14 + appbsky "github.com/bluesky-social/indigo/api/bsky" 15 + lexutil "github.com/bluesky-social/indigo/lex/util" 16 + "github.com/bluesky-social/indigo/xrpc" 17 + 18 + "github.com/brianvoe/gofakeit/v6" 19 + logging "github.com/ipfs/go-log" 20 + ) 21 + 22 + var log = logging.Logger("fakedata") 23 + 24 + func MeasureIterations(name string) func(int) { 25 + start := time.Now() 26 + return func(count int) { 27 + if count == 0 { 28 + return 29 + } 30 + total := time.Since(start) 31 + log.Infof("%s wall runtime: count=%d total=%s mean=%s", name, count, total, total/time.Duration(count)) 32 + } 33 + } 34 + 35 + func GenAccount(xrpcc *xrpc.Client, index int, accountType string) (*AccountContext, error) { 36 + var suffix string 37 + if accountType == "celebrity" { 38 + suffix = "C" 39 + } else { 40 + suffix = "" 41 + } 42 + prefix := gofakeit.Username() 43 + if len(prefix) > 10 { 44 + prefix = prefix[0:10] 45 + } 46 + handle := fmt.Sprintf("%s-%s%d.test", prefix, suffix, index) 47 + email := gofakeit.Email() 48 + password := gofakeit.Password(true, true, true, true, true, 24) 49 + ctx := context.TODO() 50 + resp, err := comatproto.ServerCreateAccount(ctx, xrpcc, &comatproto.ServerCreateAccount_Input{ 51 + Email: email, 52 + Handle: handle, 53 + Password: password, 54 + }) 55 + if err != nil { 56 + return nil, err 57 + } 58 + auth := xrpc.AuthInfo{ 59 + AccessJwt: resp.AccessJwt, 60 + RefreshJwt: resp.RefreshJwt, 61 + Handle: resp.Handle, 62 + Did: resp.Did, 63 + } 64 + return &AccountContext{ 65 + Index: index, 66 + AccountType: accountType, 67 + Email: email, 68 + Password: password, 69 + Auth: auth, 70 + }, nil 71 + } 72 + 73 + func GenProfile(xrpcc *xrpc.Client, acc *AccountContext, genAvatar, genBanner bool) error { 74 + 75 + desc := gofakeit.HipsterSentence(12) 76 + var name string 77 + if acc.AccountType == "celebrity" { 78 + name = gofakeit.CelebrityActor() 79 + } else { 80 + name = gofakeit.Name() 81 + } 82 + 83 + var avatar *lexutil.LexBlob 84 + if genAvatar { 85 + img := gofakeit.ImagePng(200, 200) 86 + resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 87 + if err != nil { 88 + return err 89 + } 90 + avatar = &lexutil.LexBlob{ 91 + Ref: resp.Blob.Ref, 92 + MimeType: "image/png", 93 + Size: resp.Blob.Size, 94 + } 95 + } 96 + var banner *lexutil.LexBlob 97 + if genBanner { 98 + img := gofakeit.ImageJpeg(800, 200) 99 + resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 100 + if err != nil { 101 + return err 102 + } 103 + banner = &lexutil.LexBlob{ 104 + Ref: resp.Blob.Ref, 105 + MimeType: "image/jpeg", 106 + Size: resp.Blob.Size, 107 + } 108 + } 109 + 110 + _, err := comatproto.RepoPutRecord(context.TODO(), xrpcc, &comatproto.RepoPutRecord_Input{ 111 + Repo: acc.Auth.Did, 112 + Collection: "app.bsky.actor.profile", 113 + Rkey: "self", 114 + Record: &lexutil.LexiconTypeDecoder{&appbsky.ActorProfile{ 115 + Description: &desc, 116 + DisplayName: &name, 117 + Avatar: avatar, 118 + Banner: banner, 119 + }}, 120 + }) 121 + return err 122 + } 123 + 124 + func GenPosts(xrpcc *xrpc.Client, catalog *AccountCatalog, acc *AccountContext, maxPosts int, fracImage float64, fracMention float64) error { 125 + 126 + var mention *appbsky.FeedPost_Entity 127 + var tgt *AccountContext 128 + var text string 129 + ctx := context.TODO() 130 + 131 + if maxPosts < 1 { 132 + return nil 133 + } 134 + count := rand.Intn(maxPosts) 135 + 136 + // celebrities make 2x the posts 137 + if acc.AccountType == "celebrity" { 138 + count = count * 2 139 + } 140 + t1 := MeasureIterations("generate posts") 141 + for i := 0; i < count; i++ { 142 + text = gofakeit.Sentence(10) 143 + if len(text) > 200 { 144 + text = text[0:200] 145 + } 146 + 147 + // half the time, mention a celeb 148 + tgt = nil 149 + mention = nil 150 + if fracMention > 0.0 && rand.Float64() < fracMention/2 { 151 + tgt = &catalog.Regulars[rand.Intn(len(catalog.Regulars))] 152 + } else if fracMention > 0.0 && rand.Float64() < fracMention/2 { 153 + tgt = &catalog.Celebs[rand.Intn(len(catalog.Celebs))] 154 + } 155 + if tgt != nil { 156 + text = "@" + tgt.Auth.Handle + " " + text 157 + mention = &appbsky.FeedPost_Entity{ 158 + Type: "mention", 159 + Value: tgt.Auth.Did, 160 + Index: &appbsky.FeedPost_TextSlice{ 161 + Start: 0, 162 + End: int64(len(tgt.Auth.Handle) + 1), 163 + }, 164 + } 165 + } 166 + 167 + var images []*appbsky.EmbedImages_Image 168 + if fracImage > 0.0 && rand.Float64() < fracImage { 169 + img := gofakeit.ImageJpeg(800, 800) 170 + resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 171 + if err != nil { 172 + return err 173 + } 174 + images = append(images, &appbsky.EmbedImages_Image{ 175 + Alt: gofakeit.Lunch(), 176 + Image: &lexutil.LexBlob{ 177 + Ref: resp.Blob.Ref, 178 + MimeType: "image/jpeg", 179 + Size: resp.Blob.Size, 180 + }, 181 + }) 182 + } 183 + post := appbsky.FeedPost{ 184 + Text: text, 185 + CreatedAt: time.Now().Format(time.RFC3339), 186 + } 187 + if mention != nil { 188 + post.Entities = []*appbsky.FeedPost_Entity{mention} 189 + } 190 + if len(images) > 0 { 191 + post.Embed = &appbsky.FeedPost_Embed{ 192 + EmbedImages: &appbsky.EmbedImages{ 193 + Images: images, 194 + }, 195 + } 196 + } 197 + if _, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{ 198 + Collection: "app.bsky.feed.post", 199 + Repo: acc.Auth.Did, 200 + Record: &lexutil.LexiconTypeDecoder{&post}, 201 + }); err != nil { 202 + return err 203 + } 204 + } 205 + t1(count) 206 + return nil 207 + } 208 + 209 + func CreateFollow(xrpcc *xrpc.Client, tgt *AccountContext) error { 210 + follow := &appbsky.GraphFollow{ 211 + CreatedAt: time.Now().Format(time.RFC3339), 212 + Subject: tgt.Auth.Did, 213 + } 214 + _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 215 + Collection: "app.bsky.graph.follow", 216 + Repo: xrpcc.Auth.Did, 217 + Record: &lexutil.LexiconTypeDecoder{follow}, 218 + }) 219 + return err 220 + } 221 + 222 + func CreateLike(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 223 + ctx := context.TODO() 224 + like := appbsky.FeedLike{ 225 + Subject: &comatproto.RepoStrongRef{ 226 + Uri: viewPost.Post.Uri, 227 + Cid: viewPost.Post.Cid, 228 + }, 229 + CreatedAt: time.Now().Format(time.RFC3339), 230 + } 231 + // TODO: may have already like? in that case should ignore error 232 + _, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{ 233 + Collection: "app.bsky.feed.like", 234 + Repo: xrpcc.Auth.Did, 235 + Record: &lexutil.LexiconTypeDecoder{&like}, 236 + }) 237 + return err 238 + } 239 + 240 + func CreateRepost(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 241 + repost := &appbsky.FeedRepost{ 242 + CreatedAt: time.Now().Format(time.RFC3339), 243 + Subject: &comatproto.RepoStrongRef{ 244 + Uri: viewPost.Post.Uri, 245 + Cid: viewPost.Post.Cid, 246 + }, 247 + } 248 + _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 249 + Collection: "app.bsky.feed.repost", 250 + Repo: xrpcc.Auth.Did, 251 + Record: &lexutil.LexiconTypeDecoder{repost}, 252 + }) 253 + return err 254 + } 255 + 256 + func CreateReply(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 257 + text := gofakeit.Sentence(10) 258 + if len(text) > 200 { 259 + text = text[0:200] 260 + } 261 + parent := &comatproto.RepoStrongRef{ 262 + Uri: viewPost.Post.Uri, 263 + Cid: viewPost.Post.Cid, 264 + } 265 + root := parent 266 + if viewPost.Reply != nil { 267 + root = &comatproto.RepoStrongRef{ 268 + Uri: viewPost.Reply.Root.Uri, 269 + Cid: viewPost.Reply.Root.Cid, 270 + } 271 + } 272 + replyPost := &appbsky.FeedPost{ 273 + CreatedAt: time.Now().Format(time.RFC3339), 274 + Text: text, 275 + Reply: &appbsky.FeedPost_ReplyRef{ 276 + Parent: parent, 277 + Root: root, 278 + }, 279 + } 280 + _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 281 + Collection: "app.bsky.feed.post", 282 + Repo: xrpcc.Auth.Did, 283 + Record: &lexutil.LexiconTypeDecoder{replyPost}, 284 + }) 285 + return err 286 + } 287 + 288 + func GenFollowsAndMutes(xrpcc *xrpc.Client, catalog *AccountCatalog, acc *AccountContext, maxFollows int, maxMutes int) error { 289 + 290 + // TODO: have a "shape" to likelihood of doing a follow 291 + var tgt *AccountContext 292 + 293 + if maxFollows > len(catalog.Regulars) { 294 + return fmt.Errorf("not enought regulars to pick maxFollowers from") 295 + } 296 + if maxMutes > len(catalog.Regulars) { 297 + return fmt.Errorf("not enought regulars to pick maxMutes from") 298 + } 299 + 300 + regCount := 0 301 + celebCount := 0 302 + if maxFollows >= 1 { 303 + regCount = rand.Intn(maxFollows) 304 + celebCount = rand.Intn(len(catalog.Celebs)) 305 + } 306 + t1 := MeasureIterations("generate follows") 307 + for idx := range rand.Perm(len(catalog.Celebs))[:celebCount] { 308 + tgt = &catalog.Celebs[idx] 309 + if tgt.Auth.Did == acc.Auth.Did { 310 + continue 311 + } 312 + if err := CreateFollow(xrpcc, tgt); err != nil { 313 + return err 314 + } 315 + } 316 + for idx := range rand.Perm(len(catalog.Regulars))[:regCount] { 317 + tgt = &catalog.Regulars[idx] 318 + if tgt.Auth.Did == acc.Auth.Did { 319 + continue 320 + } 321 + if err := CreateFollow(xrpcc, tgt); err != nil { 322 + return err 323 + } 324 + } 325 + t1(regCount + celebCount) 326 + 327 + // only muting other users, not celebs 328 + muteCount := 0 329 + if maxFollows >= 1 { 330 + muteCount = rand.Intn(maxMutes) 331 + } 332 + t2 := MeasureIterations("generate mutes") 333 + for idx := range rand.Perm(len(catalog.Regulars))[:muteCount] { 334 + tgt = &catalog.Regulars[idx] 335 + if tgt.Auth.Did == acc.Auth.Did { 336 + continue 337 + } 338 + if err := appbsky.GraphMuteActor(context.TODO(), xrpcc, &appbsky.GraphMuteActor_Input{Actor: tgt.Auth.Did}); err != nil { 339 + return err 340 + } 341 + } 342 + t2(muteCount) 343 + return nil 344 + } 345 + 346 + func GenLikesRepostsReplies(xrpcc *xrpc.Client, acc *AccountContext, fracLike, fracRepost, fracReply float64) error { 347 + // fetch timeline (up to 100), and iterate over posts 348 + maxTimeline := 100 349 + resp, err := appbsky.FeedGetTimeline(context.TODO(), xrpcc, "", "", int64(maxTimeline)) 350 + if err != nil { 351 + return err 352 + } 353 + if len(resp.Feed) > maxTimeline { 354 + return fmt.Errorf("got too long timeline len=%d", len(resp.Feed)) 355 + } 356 + for _, post := range resp.Feed { 357 + // skip account's own posts 358 + if post.Post.Author.Did == acc.Auth.Did { 359 + continue 360 + } 361 + 362 + // generate 363 + if fracLike > 0.0 && rand.Float64() < fracLike { 364 + if err := CreateLike(xrpcc, post); err != nil { 365 + return err 366 + } 367 + } 368 + if fracRepost > 0.0 && rand.Float64() < fracRepost { 369 + if err := CreateRepost(xrpcc, post); err != nil { 370 + return err 371 + } 372 + } 373 + if fracReply > 0.0 && rand.Float64() < fracReply { 374 + if err := CreateReply(xrpcc, post); err != nil { 375 + return err 376 + } 377 + } 378 + } 379 + return nil 380 + } 381 + 382 + func BrowseAccount(xrpcc *xrpc.Client, acc *AccountContext) error { 383 + // fetch notifications 384 + maxNotif := 50 385 + resp, err := appbsky.NotificationListNotifications(context.TODO(), xrpcc, "", int64(maxNotif)) 386 + if err != nil { 387 + return err 388 + } 389 + if len(resp.Notifications) > maxNotif { 390 + return fmt.Errorf("got too many notifications len=%d", len(resp.Notifications)) 391 + } 392 + t1 := MeasureIterations("notification interactions") 393 + for _, notif := range resp.Notifications { 394 + switch notif.Reason { 395 + case "vote": 396 + fallthrough 397 + case "repost": 398 + fallthrough 399 + case "follow": 400 + _, err := appbsky.ActorGetProfile(context.TODO(), xrpcc, notif.Author.Did) 401 + if err != nil { 402 + return err 403 + } 404 + _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, notif.Author.Did, "", 50) 405 + if err != nil { 406 + return err 407 + } 408 + case "mention": 409 + fallthrough 410 + case "reply": 411 + _, err := appbsky.FeedGetPostThread(context.TODO(), xrpcc, 4, notif.Uri) 412 + if err != nil { 413 + return err 414 + } 415 + default: 416 + } 417 + } 418 + t1(len(resp.Notifications)) 419 + 420 + // fetch timeline (up to 100), and iterate over posts 421 + timelineLen := 100 422 + timelineResp, err := appbsky.FeedGetTimeline(context.TODO(), xrpcc, "", "", int64(timelineLen)) 423 + if err != nil { 424 + return err 425 + } 426 + if len(timelineResp.Feed) > timelineLen { 427 + return fmt.Errorf("longer than expected timeline len=%d", len(timelineResp.Feed)) 428 + } 429 + t2 := MeasureIterations("timeline interactions") 430 + for _, post := range timelineResp.Feed { 431 + // skip account's own posts 432 + if post.Post.Author.Did == acc.Auth.Did { 433 + continue 434 + } 435 + // TODO: should we do something different here? 436 + if rand.Float64() < 0.25 { 437 + _, err = appbsky.FeedGetPostThread(context.TODO(), xrpcc, 4, post.Post.Uri) 438 + if err != nil { 439 + return err 440 + } 441 + } else if rand.Float64() < 0.25 { 442 + _, err = appbsky.ActorGetProfile(context.TODO(), xrpcc, post.Post.Author.Did) 443 + if err != nil { 444 + return err 445 + } 446 + _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, post.Post.Author.Did, "", 50) 447 + if err != nil { 448 + return err 449 + } 450 + } 451 + } 452 + t2(len(timelineResp.Feed)) 453 + 454 + // notification count for good measure 455 + _, err = appbsky.NotificationGetUnreadCount(context.TODO(), xrpcc) 456 + return err 457 + }