fork of indigo with slightly nicer lexgen
at main 12 kB view raw
1// Helpers for randomly generated app.bsky.* content: accounts, posts, likes, 2// follows, mentions, etc 3 4package fakedata 5 6import ( 7 "bytes" 8 "context" 9 "fmt" 10 "log/slog" 11 "math/rand" 12 "time" 13 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 15 appbsky "github.com/bluesky-social/indigo/api/bsky" 16 lexutil "github.com/bluesky-social/indigo/lex/util" 17 "github.com/bluesky-social/indigo/xrpc" 18 19 "github.com/brianvoe/gofakeit/v6" 20) 21 22var log = slog.Default().With("system", "fakedata") 23 24func SetLogger(logger *slog.Logger) { 25 log = logger 26} 27 28func MeasureIterations(name string) func(int) { 29 start := time.Now() 30 return func(count int) { 31 if count == 0 { 32 return 33 } 34 total := time.Since(start) 35 log.Info("wall runtime", "name", name, "count", count, "total", total, "rate", total/time.Duration(count)) 36 } 37} 38 39func GenAccount(xrpcc *xrpc.Client, index int, accountType, domainSuffix string, inviteCode *string) (*AccountContext, error) { 40 if domainSuffix == "" { 41 domainSuffix = "test" 42 } 43 var handleSuffix string 44 if accountType == "celebrity" { 45 handleSuffix = "C" 46 } else { 47 handleSuffix = "" 48 } 49 prefix := gofakeit.Username() 50 if len(prefix) > 10 { 51 prefix = prefix[0:10] 52 } 53 handle := fmt.Sprintf("%s-%s%d.%s", prefix, handleSuffix, index, domainSuffix) 54 email := gofakeit.Email() 55 password := gofakeit.Password(true, true, true, true, true, 24) 56 ctx := context.TODO() 57 resp, err := comatproto.ServerCreateAccount(ctx, xrpcc, &comatproto.ServerCreateAccount_Input{ 58 Email: &email, 59 Handle: handle, 60 InviteCode: inviteCode, 61 Password: &password, 62 }) 63 if err != nil { 64 return nil, err 65 } 66 auth := xrpc.AuthInfo{ 67 AccessJwt: resp.AccessJwt, 68 RefreshJwt: resp.RefreshJwt, 69 Handle: resp.Handle, 70 Did: resp.Did, 71 } 72 return &AccountContext{ 73 Index: index, 74 AccountType: accountType, 75 Email: email, 76 Password: password, 77 Auth: auth, 78 }, nil 79} 80 81func GenProfile(xrpcc *xrpc.Client, acc *AccountContext, genAvatar, genBanner bool) error { 82 83 desc := gofakeit.HipsterSentence(12) 84 var name string 85 if acc.AccountType == "celebrity" { 86 name = gofakeit.CelebrityActor() 87 } else { 88 name = gofakeit.Name() 89 } 90 91 var avatar *lexutil.LexBlob 92 if genAvatar { 93 img := gofakeit.ImagePng(200, 200) 94 resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 95 if err != nil { 96 return err 97 } 98 avatar = &lexutil.LexBlob{ 99 Ref: resp.Blob.Ref, 100 MimeType: "image/png", 101 Size: resp.Blob.Size, 102 } 103 } 104 var banner *lexutil.LexBlob 105 if genBanner { 106 img := gofakeit.ImageJpeg(800, 200) 107 resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 108 if err != nil { 109 return err 110 } 111 banner = &lexutil.LexBlob{ 112 Ref: resp.Blob.Ref, 113 MimeType: "image/jpeg", 114 Size: resp.Blob.Size, 115 } 116 } 117 118 _, err := comatproto.RepoPutRecord(context.TODO(), xrpcc, &comatproto.RepoPutRecord_Input{ 119 Repo: acc.Auth.Did, 120 Collection: "app.bsky.actor.profile", 121 Rkey: "self", 122 Record: &lexutil.LexiconTypeDecoder{Val: &appbsky.ActorProfile{ 123 Description: &desc, 124 DisplayName: &name, 125 Avatar: avatar, 126 Banner: banner, 127 }}, 128 }) 129 return err 130} 131 132func GenPosts(xrpcc *xrpc.Client, catalog *AccountCatalog, acc *AccountContext, maxPosts int, fracImage float64, fracMention float64) error { 133 134 var mention *appbsky.FeedPost_Entity 135 var tgt *AccountContext 136 var text string 137 ctx := context.TODO() 138 139 if maxPosts < 1 { 140 return nil 141 } 142 count := rand.Intn(maxPosts) 143 144 // celebrities make 2x the posts 145 if acc.AccountType == "celebrity" { 146 count = count * 2 147 } 148 t1 := MeasureIterations("generate posts") 149 for i := 0; i < count; i++ { 150 text = gofakeit.Sentence(10) 151 if len(text) > 200 { 152 text = text[0:200] 153 } 154 155 // half the time, mention a celeb 156 tgt = nil 157 mention = nil 158 if fracMention > 0.0 && rand.Float64() < fracMention/2 { 159 tgt = &catalog.Regulars[rand.Intn(len(catalog.Regulars))] 160 } else if fracMention > 0.0 && rand.Float64() < fracMention/2 { 161 tgt = &catalog.Celebs[rand.Intn(len(catalog.Celebs))] 162 } 163 if tgt != nil { 164 text = "@" + tgt.Auth.Handle + " " + text 165 mention = &appbsky.FeedPost_Entity{ 166 Type: "mention", 167 Value: tgt.Auth.Did, 168 Index: &appbsky.FeedPost_TextSlice{ 169 Start: 0, 170 End: int64(len(tgt.Auth.Handle) + 1), 171 }, 172 } 173 } 174 175 var images []*appbsky.EmbedImages_Image 176 if fracImage > 0.0 && rand.Float64() < fracImage { 177 img := gofakeit.ImageJpeg(800, 800) 178 resp, err := comatproto.RepoUploadBlob(context.TODO(), xrpcc, bytes.NewReader(img)) 179 if err != nil { 180 return err 181 } 182 images = append(images, &appbsky.EmbedImages_Image{ 183 Alt: gofakeit.Lunch(), 184 Image: &lexutil.LexBlob{ 185 Ref: resp.Blob.Ref, 186 MimeType: "image/jpeg", 187 Size: resp.Blob.Size, 188 }, 189 }) 190 } 191 post := appbsky.FeedPost{ 192 Text: text, 193 CreatedAt: time.Now().Format(time.RFC3339), 194 } 195 if mention != nil { 196 post.Entities = []*appbsky.FeedPost_Entity{mention} 197 } 198 if len(images) > 0 { 199 post.Embed = &appbsky.FeedPost_Embed{ 200 EmbedImages: &appbsky.EmbedImages{ 201 Images: images, 202 }, 203 } 204 } 205 if _, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{ 206 Collection: "app.bsky.feed.post", 207 Repo: acc.Auth.Did, 208 Record: &lexutil.LexiconTypeDecoder{Val: &post}, 209 }); err != nil { 210 return err 211 } 212 } 213 t1(count) 214 return nil 215} 216 217func CreateFollow(xrpcc *xrpc.Client, tgt *AccountContext) error { 218 follow := &appbsky.GraphFollow{ 219 CreatedAt: time.Now().Format(time.RFC3339), 220 Subject: tgt.Auth.Did, 221 } 222 _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 223 Collection: "app.bsky.graph.follow", 224 Repo: xrpcc.Auth.Did, 225 Record: &lexutil.LexiconTypeDecoder{Val: follow}, 226 }) 227 return err 228} 229 230func CreateLike(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 231 ctx := context.TODO() 232 like := appbsky.FeedLike{ 233 Subject: &comatproto.RepoStrongRef{ 234 Uri: viewPost.Post.Uri, 235 Cid: viewPost.Post.Cid, 236 }, 237 CreatedAt: time.Now().Format(time.RFC3339), 238 } 239 // TODO: may have already like? in that case should ignore error 240 _, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{ 241 Collection: "app.bsky.feed.like", 242 Repo: xrpcc.Auth.Did, 243 Record: &lexutil.LexiconTypeDecoder{Val: &like}, 244 }) 245 return err 246} 247 248func CreateRepost(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 249 repost := &appbsky.FeedRepost{ 250 CreatedAt: time.Now().Format(time.RFC3339), 251 Subject: &comatproto.RepoStrongRef{ 252 Uri: viewPost.Post.Uri, 253 Cid: viewPost.Post.Cid, 254 }, 255 } 256 _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 257 Collection: "app.bsky.feed.repost", 258 Repo: xrpcc.Auth.Did, 259 Record: &lexutil.LexiconTypeDecoder{Val: repost}, 260 }) 261 return err 262} 263 264func CreateReply(xrpcc *xrpc.Client, viewPost *appbsky.FeedDefs_FeedViewPost) error { 265 text := gofakeit.Sentence(10) 266 if len(text) > 200 { 267 text = text[0:200] 268 } 269 parent := &comatproto.RepoStrongRef{ 270 Uri: viewPost.Post.Uri, 271 Cid: viewPost.Post.Cid, 272 } 273 root := parent 274 if viewPost.Reply != nil { 275 root = &comatproto.RepoStrongRef{ 276 Uri: viewPost.Reply.Root.FeedDefs_PostView.Uri, 277 Cid: viewPost.Reply.Root.FeedDefs_PostView.Cid, 278 } 279 } 280 replyPost := &appbsky.FeedPost{ 281 CreatedAt: time.Now().Format(time.RFC3339), 282 Text: text, 283 Reply: &appbsky.FeedPost_ReplyRef{ 284 Parent: parent, 285 Root: root, 286 }, 287 } 288 _, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{ 289 Collection: "app.bsky.feed.post", 290 Repo: xrpcc.Auth.Did, 291 Record: &lexutil.LexiconTypeDecoder{Val: replyPost}, 292 }) 293 return err 294} 295 296func GenFollowsAndMutes(xrpcc *xrpc.Client, catalog *AccountCatalog, acc *AccountContext, maxFollows int, maxMutes int) error { 297 298 // TODO: have a "shape" to likelihood of doing a follow 299 var tgt *AccountContext 300 301 if maxFollows > len(catalog.Regulars) { 302 return fmt.Errorf("not enough regulars to pick maxFollowers from") 303 } 304 if maxMutes > len(catalog.Regulars) { 305 return fmt.Errorf("not enough regulars to pick maxMutes from") 306 } 307 308 regCount := 0 309 celebCount := 0 310 if maxFollows >= 1 { 311 regCount = rand.Intn(maxFollows) 312 celebCount = rand.Intn(len(catalog.Celebs)) 313 } 314 t1 := MeasureIterations("generate follows") 315 for idx := range rand.Perm(len(catalog.Celebs))[:celebCount] { 316 tgt = &catalog.Celebs[idx] 317 if tgt.Auth.Did == acc.Auth.Did { 318 continue 319 } 320 if err := CreateFollow(xrpcc, tgt); err != nil { 321 return err 322 } 323 } 324 for idx := range rand.Perm(len(catalog.Regulars))[:regCount] { 325 tgt = &catalog.Regulars[idx] 326 if tgt.Auth.Did == acc.Auth.Did { 327 continue 328 } 329 if err := CreateFollow(xrpcc, tgt); err != nil { 330 return err 331 } 332 } 333 t1(regCount + celebCount) 334 335 // only muting other users, not celebs 336 muteCount := 0 337 if maxFollows >= 1 && maxMutes > 0 { 338 muteCount = rand.Intn(maxMutes) 339 } 340 t2 := MeasureIterations("generate mutes") 341 for idx := range rand.Perm(len(catalog.Regulars))[:muteCount] { 342 tgt = &catalog.Regulars[idx] 343 if tgt.Auth.Did == acc.Auth.Did { 344 continue 345 } 346 if err := appbsky.GraphMuteActor(context.TODO(), xrpcc, &appbsky.GraphMuteActor_Input{Actor: tgt.Auth.Did}); err != nil { 347 return err 348 } 349 } 350 t2(muteCount) 351 return nil 352} 353 354func GenLikesRepostsReplies(xrpcc *xrpc.Client, acc *AccountContext, fracLike, fracRepost, fracReply float64) error { 355 // fetch timeline (up to 100), and iterate over posts 356 maxTimeline := 100 357 resp, err := appbsky.FeedGetTimeline(context.TODO(), xrpcc, "", "", int64(maxTimeline)) 358 if err != nil { 359 return err 360 } 361 if len(resp.Feed) > maxTimeline { 362 return fmt.Errorf("got too long timeline len=%d", len(resp.Feed)) 363 } 364 for _, post := range resp.Feed { 365 // skip account's own posts 366 if post.Post.Author.Did == acc.Auth.Did { 367 continue 368 } 369 370 // generate 371 if fracLike > 0.0 && rand.Float64() < fracLike { 372 if err := CreateLike(xrpcc, post); err != nil { 373 return err 374 } 375 } 376 if fracRepost > 0.0 && rand.Float64() < fracRepost { 377 if err := CreateRepost(xrpcc, post); err != nil { 378 return err 379 } 380 } 381 if fracReply > 0.0 && rand.Float64() < fracReply { 382 if err := CreateReply(xrpcc, post); err != nil { 383 return err 384 } 385 } 386 } 387 return nil 388} 389 390func BrowseAccount(xrpcc *xrpc.Client, acc *AccountContext) error { 391 // fetch notifications 392 maxNotif := 50 393 resp, err := appbsky.NotificationListNotifications(context.TODO(), xrpcc, "", int64(maxNotif), false, nil, "") 394 if err != nil { 395 return err 396 } 397 if len(resp.Notifications) > maxNotif { 398 return fmt.Errorf("got too many notifications len=%d", len(resp.Notifications)) 399 } 400 t1 := MeasureIterations("notification interactions") 401 for _, notif := range resp.Notifications { 402 switch notif.Reason { 403 case "vote": 404 fallthrough 405 case "repost": 406 fallthrough 407 case "follow": 408 _, err := appbsky.ActorGetProfile(context.TODO(), xrpcc, notif.Author.Did) 409 if err != nil { 410 return err 411 } 412 _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, notif.Author.Did, "", "", false, 50) 413 if err != nil { 414 return err 415 } 416 case "mention": 417 fallthrough 418 case "reply": 419 _, err := appbsky.FeedGetPostThread(context.TODO(), xrpcc, 4, 80, notif.Uri) 420 if err != nil { 421 return err 422 } 423 default: 424 } 425 } 426 t1(len(resp.Notifications)) 427 428 // fetch timeline (up to 100), and iterate over posts 429 timelineLen := 100 430 timelineResp, err := appbsky.FeedGetTimeline(context.TODO(), xrpcc, "", "", int64(timelineLen)) 431 if err != nil { 432 return err 433 } 434 if len(timelineResp.Feed) > timelineLen { 435 return fmt.Errorf("longer than expected timeline len=%d", len(timelineResp.Feed)) 436 } 437 t2 := MeasureIterations("timeline interactions") 438 for _, post := range timelineResp.Feed { 439 // skip account's own posts 440 if post.Post.Author.Did == acc.Auth.Did { 441 continue 442 } 443 // TODO: should we do something different here? 444 if rand.Float64() < 0.25 { 445 _, err = appbsky.FeedGetPostThread(context.TODO(), xrpcc, 4, 80, post.Post.Uri) 446 if err != nil { 447 return err 448 } 449 } else if rand.Float64() < 0.25 { 450 _, err = appbsky.ActorGetProfile(context.TODO(), xrpcc, post.Post.Author.Did) 451 if err != nil { 452 return err 453 } 454 _, err = appbsky.FeedGetAuthorFeed(context.TODO(), xrpcc, post.Post.Author.Did, "", "", false, 50) 455 if err != nil { 456 return err 457 } 458 } 459 } 460 t2(len(timelineResp.Feed)) 461 462 // notification count for good measure 463 _, err = appbsky.NotificationGetUnreadCount(context.TODO(), xrpcc, false, "") 464 return err 465}