porting all github actions from bluesky-social/indigo to tangled CI
at main 11 kB view raw
1// Tool to generate fake accounts, content, and interactions. 2// Intended for development and benchmarking. Similar to 'stress' and could 3// merge at some point. 4 5package main 6 7import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "os" 12 "runtime" 13 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 15 "github.com/bluesky-social/indigo/fakedata" 16 "github.com/bluesky-social/indigo/util/cliutil" 17 18 _ "github.com/joho/godotenv/autoload" 19 _ "go.uber.org/automaxprocs" 20 21 "github.com/carlmjohnson/versioninfo" 22 "github.com/urfave/cli/v2" 23 "golang.org/x/sync/errgroup" 24) 25 26func main() { 27 run(os.Args) 28} 29 30func run(args []string) { 31 32 app := cli.App{ 33 Name: "fakermaker", 34 Usage: "bluesky fake account/content generator", 35 Version: versioninfo.Short(), 36 } 37 38 app.Flags = []cli.Flag{ 39 &cli.StringFlag{ 40 Name: "pds-host", 41 Usage: "method, hostname, and port of PDS instance", 42 Value: "http://localhost:4849", 43 EnvVars: []string{"ATP_PDS_HOST"}, 44 }, 45 &cli.StringFlag{ 46 Name: "admin-password", 47 Usage: "admin authentication password for PDS", 48 Required: true, 49 EnvVars: []string{"ATP_AUTH_ADMIN_PASSWORD"}, 50 }, 51 &cli.IntFlag{ 52 Name: "jobs", 53 Aliases: []string{"j"}, 54 Usage: "number of parallel threads to use", 55 Value: runtime.NumCPU(), 56 }, 57 } 58 app.Commands = []*cli.Command{ 59 &cli.Command{ 60 Name: "gen-accounts", 61 Usage: "create accounts (DID, handle, profile)", 62 Action: genAccounts, 63 Flags: []cli.Flag{ 64 &cli.IntFlag{ 65 Name: "count", 66 Aliases: []string{"n"}, 67 Usage: "total number of accounts to create", 68 Value: 100, 69 }, 70 &cli.IntFlag{ 71 Name: "count-celebrities", 72 Usage: "number of accounts as 'celebrities' (many followers)", 73 Value: 10, 74 }, 75 &cli.StringFlag{ 76 Name: "domain-suffix", 77 Usage: "domain to register handle under", 78 Value: "test", 79 }, 80 &cli.BoolFlag{ 81 Name: "use-invite-code", 82 Usage: "create and use an invite code", 83 Value: false, 84 }, 85 }, 86 }, 87 &cli.Command{ 88 Name: "gen-profiles", 89 Usage: "creates profile records for accounts", 90 Action: genProfiles, 91 Flags: []cli.Flag{ 92 &cli.StringFlag{ 93 Name: "catalog", 94 Usage: "file path of account catalog JSON file", 95 Value: "data/fakermaker/accounts.json", 96 }, 97 &cli.BoolFlag{ 98 Name: "no-avatars", 99 Usage: "disable avatar image generation", 100 Value: false, 101 }, 102 &cli.BoolFlag{ 103 Name: "no-banners", 104 Usage: "disable profile banner image generation", 105 Value: false, 106 }, 107 }, 108 }, 109 &cli.Command{ 110 Name: "gen-graph", 111 Usage: "creates social graph (follows and mutes)", 112 Action: genGraph, 113 Flags: []cli.Flag{ 114 &cli.StringFlag{ 115 Name: "catalog", 116 Usage: "file path of account catalog JSON file", 117 Value: "data/fakermaker/accounts.json", 118 }, 119 &cli.IntFlag{ 120 Name: "max-follows", 121 Usage: "create up to this many follows for each account", 122 Value: 90, 123 }, 124 &cli.IntFlag{ 125 Name: "max-mutes", 126 Usage: "create up to this many mutes (blocks) for each account", 127 Value: 25, 128 }, 129 }, 130 }, 131 &cli.Command{ 132 Name: "gen-posts", 133 Usage: "creates posts for accounts", 134 Action: genPosts, 135 Flags: []cli.Flag{ 136 &cli.StringFlag{ 137 Name: "catalog", 138 Usage: "file path of account catalog JSON file", 139 Value: "data/fakermaker/accounts.json", 140 }, 141 &cli.IntFlag{ 142 Name: "max-posts", 143 Usage: "create up to this many posts for each account", 144 Value: 10, 145 }, 146 &cli.Float64Flag{ 147 Name: "frac-image", 148 Usage: "portion of posts to include images", 149 Value: 0.25, 150 }, 151 &cli.Float64Flag{ 152 Name: "frac-mention", 153 Usage: "of posts created, fraction to include mentions in", 154 Value: 0.50, 155 }, 156 }, 157 }, 158 &cli.Command{ 159 Name: "gen-interactions", 160 Usage: "create interactions between accounts", 161 Action: genInteractions, 162 Flags: []cli.Flag{ 163 &cli.StringFlag{ 164 Name: "catalog", 165 Usage: "file path of account catalog JSON file", 166 Value: "data/fakermaker/accounts.json", 167 }, 168 &cli.Float64Flag{ 169 Name: "frac-like", 170 Usage: "fraction of posts in timeline to like", 171 Value: 0.20, 172 }, 173 &cli.Float64Flag{ 174 Name: "frac-repost", 175 Usage: "fraction of posts in timeline to repost", 176 Value: 0.20, 177 }, 178 &cli.Float64Flag{ 179 Name: "frac-reply", 180 Usage: "fraction of posts in timeline to reply to", 181 Value: 0.20, 182 }, 183 }, 184 }, 185 &cli.Command{ 186 Name: "run-browsing", 187 Usage: "creates read-only load on service (notifications, timeline, etc)", 188 Action: runBrowsing, 189 Flags: []cli.Flag{ 190 &cli.StringFlag{ 191 Name: "catalog", 192 Usage: "file path of account catalog JSON file", 193 Value: "data/fakermaker/accounts.json", 194 }, 195 }, 196 }, 197 } 198 all := fakedata.MeasureIterations("entire command") 199 app.RunAndExitOnError() 200 all(1) 201} 202 203// registers fake accounts with PDS, and spits out JSON-lines to stdout with auth info 204func genAccounts(cctx *cli.Context) error { 205 206 // establish atproto client, with admin token for auth 207 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 208 if err != nil { 209 return err 210 } 211 adminToken := cctx.String("admin-password") 212 if len(adminToken) > 0 { 213 xrpcc.AdminToken = &adminToken 214 } 215 216 countTotal := cctx.Int("count") 217 countCelebrities := cctx.Int("count-celebrities") 218 domainSuffix := cctx.String("domain-suffix") 219 if countCelebrities > countTotal { 220 return fmt.Errorf("more celebrities than total accounts!") 221 } 222 countRegulars := countTotal - countCelebrities 223 224 var inviteCode *string = nil 225 if cctx.Bool("use-invite-code") { 226 resp, err := comatproto.ServerCreateInviteCodes(context.TODO(), xrpcc, &comatproto.ServerCreateInviteCodes_Input{ 227 UseCount: int64(countTotal), 228 ForAccounts: nil, 229 CodeCount: 1, 230 }) 231 if err != nil { 232 return err 233 } 234 if len(resp.Codes) != 1 || len(resp.Codes[0].Codes) != 1 { 235 return fmt.Errorf("expected a single invite code") 236 } 237 inviteCode = &resp.Codes[0].Codes[0] 238 } 239 240 // call helper to do actual creation 241 var usr *fakedata.AccountContext 242 var line []byte 243 t1 := fakedata.MeasureIterations("register celebrity accounts") 244 for i := 0; i < countCelebrities; i++ { 245 if usr, err = fakedata.GenAccount(xrpcc, i, "celebrity", domainSuffix, inviteCode); err != nil { 246 return err 247 } 248 // compact single-line JSON by default 249 if line, err = json.Marshal(usr); err != nil { 250 return nil 251 } 252 fmt.Println(string(line)) 253 } 254 t1(countCelebrities) 255 256 t2 := fakedata.MeasureIterations("register regular accounts") 257 for i := 0; i < countRegulars; i++ { 258 if usr, err = fakedata.GenAccount(xrpcc, i, "regular", domainSuffix, inviteCode); err != nil { 259 return err 260 } 261 // compact single-line JSON by default 262 if line, err = json.Marshal(usr); err != nil { 263 return nil 264 } 265 fmt.Println(string(line)) 266 } 267 t2(countRegulars) 268 return nil 269} 270 271func genProfiles(cctx *cli.Context) error { 272 catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 273 if err != nil { 274 return err 275 } 276 277 pdsHost := cctx.String("pds-host") 278 genAvatar := !cctx.Bool("no-avatars") 279 genBanner := !cctx.Bool("no-banners") 280 jobs := cctx.Int("jobs") 281 282 accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 283 eg := new(errgroup.Group) 284 for i := 0; i < jobs; i++ { 285 eg.Go(func() error { 286 for acc := range accChan { 287 xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 288 if err != nil { 289 return err 290 } 291 if err = fakedata.GenProfile(xrpcc, &acc, genAvatar, genBanner); err != nil { 292 return err 293 } 294 } 295 return nil 296 }) 297 } 298 299 for _, acc := range append(catalog.Celebs, catalog.Regulars...) { 300 accChan <- acc 301 } 302 close(accChan) 303 return eg.Wait() 304} 305 306func genGraph(cctx *cli.Context) error { 307 catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 308 if err != nil { 309 return err 310 } 311 312 pdsHost := cctx.String("pds-host") 313 maxFollows := cctx.Int("max-follows") 314 maxMutes := cctx.Int("max-mutes") 315 jobs := cctx.Int("jobs") 316 317 accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 318 eg := new(errgroup.Group) 319 for i := 0; i < jobs; i++ { 320 eg.Go(func() error { 321 for acc := range accChan { 322 xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 323 if err != nil { 324 return err 325 } 326 if err = fakedata.GenFollowsAndMutes(xrpcc, catalog, &acc, maxFollows, maxMutes); err != nil { 327 return err 328 } 329 } 330 return nil 331 }) 332 } 333 334 for _, acc := range append(catalog.Celebs, catalog.Regulars...) { 335 accChan <- acc 336 } 337 close(accChan) 338 return eg.Wait() 339} 340 341func genPosts(cctx *cli.Context) error { 342 catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 343 if err != nil { 344 return err 345 } 346 347 pdsHost := cctx.String("pds-host") 348 maxPosts := cctx.Int("max-posts") 349 fracImage := cctx.Float64("frac-image") 350 fracMention := cctx.Float64("frac-mention") 351 jobs := cctx.Int("jobs") 352 353 accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 354 eg := new(errgroup.Group) 355 for i := 0; i < jobs; i++ { 356 eg.Go(func() error { 357 for acc := range accChan { 358 xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 359 if err != nil { 360 return err 361 } 362 if err = fakedata.GenPosts(xrpcc, catalog, &acc, maxPosts, fracImage, fracMention); err != nil { 363 return err 364 } 365 } 366 return nil 367 }) 368 } 369 370 for _, acc := range append(catalog.Celebs, catalog.Regulars...) { 371 accChan <- acc 372 } 373 close(accChan) 374 return eg.Wait() 375} 376 377func genInteractions(cctx *cli.Context) error { 378 catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 379 if err != nil { 380 return err 381 } 382 383 pdsHost := cctx.String("pds-host") 384 fracLike := cctx.Float64("frac-like") 385 fracRepost := cctx.Float64("frac-repost") 386 fracReply := cctx.Float64("frac-reply") 387 jobs := cctx.Int("jobs") 388 389 accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 390 eg := new(errgroup.Group) 391 for i := 0; i < jobs; i++ { 392 eg.Go(func() error { 393 for acc := range accChan { 394 xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 395 if err != nil { 396 return err 397 } 398 t1 := fakedata.MeasureIterations("all interactions") 399 if err := fakedata.GenLikesRepostsReplies(xrpcc, &acc, fracLike, fracRepost, fracReply); err != nil { 400 return err 401 } 402 t1(1) 403 } 404 return nil 405 }) 406 } 407 408 for _, acc := range append(catalog.Celebs, catalog.Regulars...) { 409 accChan <- acc 410 } 411 close(accChan) 412 return eg.Wait() 413} 414 415func runBrowsing(cctx *cli.Context) error { 416 catalog, err := fakedata.ReadAccountCatalog(cctx.String("catalog")) 417 if err != nil { 418 return err 419 } 420 421 pdsHost := cctx.String("pds-host") 422 jobs := cctx.Int("jobs") 423 424 accChan := make(chan fakedata.AccountContext, len(catalog.Celebs)+len(catalog.Regulars)) 425 eg := new(errgroup.Group) 426 for i := 0; i < jobs; i++ { 427 eg.Go(func() error { 428 for acc := range accChan { 429 xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 430 if err != nil { 431 return err 432 } 433 if err := fakedata.BrowseAccount(xrpcc, &acc); err != nil { 434 return err 435 } 436 } 437 return nil 438 }) 439 } 440 441 for _, acc := range append(catalog.Celebs, catalog.Regulars...) { 442 accChan <- acc 443 } 444 close(accChan) 445 return eg.Wait() 446}