fork of indigo with slightly nicer lexgen
at main 19 kB view raw
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "os" 8 "sort" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/bluesky-social/indigo/api/atproto" 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 15 toolsozone "github.com/bluesky-social/indigo/api/ozone" 16 "github.com/bluesky-social/indigo/atproto/identity" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 "github.com/bluesky-social/indigo/handles" 19 "github.com/bluesky-social/indigo/util/cliutil" 20 cli "github.com/urfave/cli/v2" 21) 22 23var adminCmd = &cli.Command{ 24 Name: "admin", 25 Usage: "sub-commands for PDS administration", 26 Flags: []cli.Flag{ 27 &cli.StringFlag{ 28 Name: "admin-password", 29 EnvVars: []string{"ATP_AUTH_ADMIN_PASSWORD"}, 30 Required: true, 31 }, 32 &cli.StringFlag{ 33 Name: "admin-endpoint", 34 Value: "https://mod.bsky.app", 35 }, 36 }, 37 Subcommands: []*cli.Command{ 38 buildInviteTreeCmd, 39 checkUserCmd, 40 createInviteCmd, 41 disableInvitesCmd, 42 enableInvitesCmd, 43 queryModerationStatusesCmd, 44 listInviteTreeCmd, 45 reportsCmd, 46 takeDownAccountCmd, 47 }, 48} 49 50var checkUserCmd = &cli.Command{ 51 Name: "check-user", 52 Flags: []cli.Flag{ 53 &cli.BoolFlag{ 54 Name: "raw", 55 Usage: "dump simple JSON response to stdout", 56 }, 57 &cli.BoolFlag{ 58 Name: "list-invited-dids", 59 }, 60 }, 61 ArgsUsage: `<did-or-handle>`, 62 Action: func(cctx *cli.Context) error { 63 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 64 if err != nil { 65 return err 66 } 67 68 dir := identity.DefaultDirectory() 69 ctx := context.Background() 70 71 ident, err := syntax.ParseAtIdentifier(cctx.Args().First()) 72 if err != nil { 73 return err 74 } 75 76 id, err := dir.Lookup(ctx, *ident) 77 if err != nil { 78 return fmt.Errorf("resolve identifier %q: %w", cctx.Args().First(), err) 79 } 80 81 did := id.DID.String() 82 83 adminKey := cctx.String("admin-password") 84 xrpcc.AdminToken = &adminKey 85 xrpcc.Host = cctx.String("admin-endpoint") 86 87 rep, err := toolsozone.ModerationGetRepo(ctx, xrpcc, did) 88 if err != nil { 89 return fmt.Errorf("getRepo %s: %w", did, err) 90 } 91 92 b, err := json.MarshalIndent(rep, "", " ") 93 if err != nil { 94 return err 95 } 96 97 if cctx.Bool("raw") { 98 fmt.Println(string(b)) 99 } else if cctx.Bool("list-invited-dids") { 100 for _, inv := range rep.Invites { 101 for _, u := range inv.Uses { 102 fmt.Println(u.UsedBy) 103 } 104 } 105 } else { 106 var invby string 107 if rep.InvitedBy != nil { 108 if fa := rep.InvitedBy.ForAccount; fa != "" { 109 if fa == "admin" { 110 invby = fa 111 } else { 112 id, err := dir.LookupDID(ctx, syntax.DID(fa)) 113 if err != nil { 114 fmt.Println("ERROR: failed to resolve inviter: ", err) 115 } 116 117 invby = id.Handle.String() 118 } 119 } 120 } 121 122 fmt.Println(rep.Handle) 123 fmt.Println(rep.Did) 124 if rep.Email != nil { 125 fmt.Println(*rep.Email) 126 } 127 fmt.Println("indexed at: ", rep.IndexedAt) 128 fmt.Printf("Invited by: %s\n", invby) 129 if rep.InvitesDisabled != nil && *rep.InvitesDisabled { 130 fmt.Println("INVITES DISABLED") 131 } 132 133 var invited []*toolsozone.ModerationDefs_RepoViewDetail 134 var lk sync.Mutex 135 var wg sync.WaitGroup 136 var used int 137 var revoked int 138 for _, inv := range rep.Invites { 139 used += len(inv.Uses) 140 141 if inv.Disabled { 142 revoked++ 143 } 144 for _, u := range inv.Uses { 145 wg.Add(1) 146 go func(did string) { 147 defer wg.Done() 148 repo, err := toolsozone.ModerationGetRepo(ctx, xrpcc, did) 149 if err != nil { 150 fmt.Println("ERROR: ", err) 151 return 152 } 153 154 lk.Lock() 155 invited = append(invited, repo) 156 lk.Unlock() 157 }(u.UsedBy) 158 } 159 } 160 161 wg.Wait() 162 163 fmt.Printf("Invites, used %d of %d (%d disabled)\n", used, len(rep.Invites), revoked) 164 for _, inv := range invited { 165 166 var invited, total int 167 for _, code := range inv.Invites { 168 total += len(code.Uses) + int(code.Available) 169 invited += len(code.Uses) 170 } 171 172 fmt.Printf(" - %s (%d / %d)\n", inv.Handle, invited, total) 173 } 174 } 175 return nil 176 }, 177} 178 179var buildInviteTreeCmd = &cli.Command{ 180 Name: "build-invite-tree", 181 Flags: []cli.Flag{ 182 &cli.StringFlag{ 183 Name: "invite-list", 184 }, 185 &cli.IntFlag{ 186 Name: "top", 187 Value: 50, 188 }, 189 }, 190 Action: func(cctx *cli.Context) error { 191 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 192 if err != nil { 193 return err 194 } 195 196 ctx := context.Background() 197 198 adminKey := cctx.String("admin-password") 199 200 xrpcc.AdminToken = &adminKey 201 202 var allcodes []*atproto.ServerDefs_InviteCode 203 204 if invl := cctx.String("invite-list"); invl != "" { 205 fi, err := os.Open(invl) 206 if err != nil { 207 return err 208 } 209 210 if err := json.NewDecoder(fi).Decode(&allcodes); err != nil { 211 return err 212 } 213 } else { 214 var cursor string 215 for { 216 invites, err := atproto.AdminGetInviteCodes(ctx, xrpcc, cursor, 100, "") 217 if err != nil { 218 return err 219 } 220 221 allcodes = append(allcodes, invites.Codes...) 222 223 if invites.Cursor != nil { 224 cursor = *invites.Cursor 225 } 226 if len(invites.Codes) == 0 { 227 break 228 } 229 } 230 231 fi, err := os.Create("output.json") 232 if err != nil { 233 return err 234 } 235 defer fi.Close() 236 237 if err := json.NewEncoder(fi).Encode(allcodes); err != nil { 238 return err 239 } 240 } 241 242 users := make(map[string]*userInviteInfo) 243 users["admin"] = &userInviteInfo{ 244 Handle: "admin", 245 } 246 247 var getUser func(did string) (*userInviteInfo, error) 248 getUser = func(did string) (*userInviteInfo, error) { 249 u, ok := users[did] 250 if ok { 251 return u, nil 252 } 253 254 repo, err := toolsozone.ModerationGetRepo(ctx, xrpcc, did) 255 if err != nil { 256 return nil, err 257 } 258 259 var invby string 260 if fa := repo.InvitedBy.ForAccount; fa != "" { 261 if fa == "admin" { 262 invby = "admin" 263 } else { 264 invu, ok := users[fa] 265 if ok { 266 invby = invu.Handle 267 } else { 268 invrepo, err := toolsozone.ModerationGetRepo(ctx, xrpcc, fa) 269 if err != nil { 270 return nil, fmt.Errorf("resolving inviter (%q): %w", fa, err) 271 } 272 273 invby = invrepo.Handle 274 } 275 } 276 } 277 278 u = &userInviteInfo{ 279 Did: did, 280 Handle: repo.Handle, 281 InvitedBy: repo.InvitedBy.ForAccount, 282 InvitedByHandle: invby, 283 TotalInvites: len(repo.Invites), 284 } 285 if repo.Email != nil { 286 u.Email = *repo.Email 287 } 288 289 users[did] = u 290 291 return u, nil 292 } 293 _ = getUser 294 295 initmap := make(map[string]*basicInvInfo) 296 var initlist []*basicInvInfo 297 for _, inv := range allcodes { 298 acc, ok := initmap[inv.ForAccount] 299 if !ok { 300 acc = &basicInvInfo{ 301 Did: inv.ForAccount, 302 } 303 initmap[inv.ForAccount] = acc 304 initlist = append(initlist, acc) 305 } 306 307 acc.TotalInvites += int(inv.Available) + len(inv.Uses) 308 for _, u := range inv.Uses { 309 acc.Invited = append(acc.Invited, u.UsedBy) 310 } 311 } 312 313 sort.Slice(initlist, func(i, j int) bool { 314 return len(initlist[i].Invited) > len(initlist[j].Invited) 315 }) 316 317 for i := 0; i < cctx.Int("top"); i++ { 318 u, err := getUser(initlist[i].Did) 319 if err != nil { 320 fmt.Printf("getuser %q: %s\n", initlist[i].Did, err) 321 continue 322 } 323 324 fmt.Printf("%d: %s (%d of %d)\n", i, u.Handle, len(initlist[i].Invited), u.TotalInvites) 325 } 326 327 /* 328 fmt.Println("writing output...") 329 outfi, err := os.Create("userdump.json") 330 if err != nil { 331 return err 332 } 333 defer outfi.Close() 334 335 return json.NewEncoder(outfi).Encode(users) 336 */ 337 338 return nil 339 }, 340} 341 342type userInviteInfo struct { 343 CreatedAt time.Time 344 Did string 345 Handle string 346 InvitedBy string 347 InvitedByHandle string 348 TotalInvites int 349 Invited []string 350 Email string 351} 352 353type basicInvInfo struct { 354 Did string 355 Invited []string 356 TotalInvites int 357} 358 359var reportsCmd = &cli.Command{ 360 Name: "reports", 361 Subcommands: []*cli.Command{ 362 listReportsCmd, 363 }, 364} 365 366var listReportsCmd = &cli.Command{ 367 Name: "list", 368 Flags: []cli.Flag{ 369 &cli.BoolFlag{ 370 Name: "raw", 371 }, 372 &cli.BoolFlag{ 373 Name: "resolved", 374 Value: true, 375 }, 376 &cli.BoolFlag{ 377 Name: "template-output", 378 }, 379 }, 380 Action: func(cctx *cli.Context) error { 381 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 382 if err != nil { 383 return err 384 } 385 386 ctx := context.Background() 387 388 adminKey := cctx.String("admin-password") 389 xrpcc.AdminToken = &adminKey 390 391 // fetch recent moderation reports 392 resp, err := toolsozone.ModerationQueryEvents( 393 ctx, 394 xrpcc, 395 nil, // addedLabels []string 396 nil, // addedTags []string 397 "", // ageAssuranceState 398 nil, // collections []string 399 "", // comment string 400 "", // createdAfter string 401 "", // createdBefore string 402 "", // createdBy string 403 "", // cursor string 404 false, // hasComment bool 405 false, // includeAllUserRecords bool 406 100, // limit int64 407 nil, // modTool 408 nil, // policies []string 409 nil, // removedLabels []string 410 nil, // removedTags []string 411 nil, // reportTypes []string 412 "", // sortDirection string 413 "", // subject string 414 "", // subjectType string 415 []string{"tools.ozone.moderation.defs#modEventReport"}, // types []string 416 ) 417 if err != nil { 418 return err 419 } 420 421 for _, rep := range resp.Events { 422 b, err := json.MarshalIndent(rep, "", " ") 423 if err != nil { 424 return err 425 } 426 fmt.Println(string(b)) 427 } 428 return nil 429 }, 430} 431 432var disableInvitesCmd = &cli.Command{ 433 Name: "disable-invites", 434 ArgsUsage: "<did-or-handle>", 435 Action: func(cctx *cli.Context) error { 436 437 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 438 if err != nil { 439 return err 440 } 441 442 ctx := context.Background() 443 444 adminKey := cctx.String("admin-password") 445 xrpcc.AdminToken = &adminKey 446 447 phr := &handles.ProdHandleResolver{} 448 handle := cctx.Args().First() 449 if !strings.HasPrefix(handle, "did:") { 450 resp, err := phr.ResolveHandleToDid(ctx, handle) 451 if err != nil { 452 return err 453 } 454 455 handle = resp 456 } 457 458 if err := atproto.AdminDisableAccountInvites(ctx, xrpcc, &atproto.AdminDisableAccountInvites_Input{ 459 Account: handle, 460 }); err != nil { 461 return err 462 } 463 464 if err := atproto.AdminDisableInviteCodes(ctx, xrpcc, &atproto.AdminDisableInviteCodes_Input{ 465 Accounts: []string{handle}, 466 }); err != nil { 467 return err 468 } 469 470 return nil 471 }, 472} 473 474var enableInvitesCmd = &cli.Command{ 475 Name: "enable-invites", 476 ArgsUsage: "<did-or-handle>", 477 Action: func(cctx *cli.Context) error { 478 479 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 480 if err != nil { 481 return err 482 } 483 484 ctx := context.Background() 485 486 adminKey := cctx.String("admin-password") 487 xrpcc.AdminToken = &adminKey 488 489 handle := cctx.Args().First() 490 if !strings.HasPrefix(handle, "did:") { 491 phr := &handles.ProdHandleResolver{} 492 resp, err := phr.ResolveHandleToDid(ctx, handle) 493 if err != nil { 494 return err 495 } 496 497 handle = resp 498 } 499 500 return atproto.AdminEnableAccountInvites(ctx, xrpcc, &atproto.AdminEnableAccountInvites_Input{ 501 Account: handle, 502 }) 503 }, 504} 505 506var listInviteTreeCmd = &cli.Command{ 507 Name: "list-invite-tree", 508 Flags: []cli.Flag{ 509 &cli.BoolFlag{ 510 Name: "disable-invites", 511 Usage: "additionally disable invites for all printed DIDs", 512 }, 513 &cli.BoolFlag{ 514 Name: "revoke-existing-invites", 515 Usage: "additionally revoke any existing invites for all printed DIDs", 516 }, 517 &cli.BoolFlag{ 518 Name: "print-handles", 519 Usage: "print handle for each DID", 520 }, 521 &cli.BoolFlag{ 522 Name: "print-emails", 523 Usage: "print account email for each DID", 524 }, 525 }, 526 ArgsUsage: `<did-or-handle>`, 527 Action: func(cctx *cli.Context) error { 528 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 529 if err != nil { 530 return err 531 } 532 533 ctx := context.Background() 534 535 phr := &handles.ProdHandleResolver{} 536 537 did := cctx.Args().First() 538 if !strings.HasPrefix(did, "did:") { 539 rdid, err := phr.ResolveHandleToDid(ctx, cctx.Args().First()) 540 if err != nil { 541 return fmt.Errorf("resolve handle %q: %w", cctx.Args().First(), err) 542 } 543 544 did = rdid 545 } 546 547 adminKey := cctx.String("admin-password") 548 xrpcc.AdminToken = &adminKey 549 550 queue := []string{did} 551 552 for len(queue) > 0 { 553 next := queue[0] 554 queue = queue[1:] 555 556 if cctx.Bool("disable-invites") { 557 if err := atproto.AdminDisableAccountInvites(ctx, xrpcc, &atproto.AdminDisableAccountInvites_Input{ 558 Account: next, 559 }); err != nil { 560 return fmt.Errorf("failed to disable invites on %q: %w", next, err) 561 } 562 } 563 564 if cctx.Bool("revoke-existing-invites") { 565 if err := atproto.AdminDisableInviteCodes(ctx, xrpcc, &atproto.AdminDisableInviteCodes_Input{ 566 Accounts: []string{next}, 567 }); err != nil { 568 return fmt.Errorf("failed to revoke existing invites on %q: %w", next, err) 569 } 570 } 571 572 rep, err := toolsozone.ModerationGetRepo(ctx, xrpcc, next) 573 if err != nil { 574 fmt.Printf("Failed to getRepo for DID %s: %s\n", next, err.Error()) 575 continue 576 } 577 fmt.Print(next) 578 579 if cctx.Bool("print-handles") { 580 if rep.Handle != "" { 581 fmt.Print(" ", rep.Handle) 582 } else { 583 fmt.Print(" NO HANDLE") 584 } 585 } 586 587 if cctx.Bool("print-emails") { 588 if rep.Email != nil { 589 fmt.Print(" ", *rep.Email) 590 } else { 591 fmt.Print(" NO EMAIL") 592 } 593 } 594 fmt.Println() 595 596 for _, inv := range rep.Invites { 597 for _, u := range inv.Uses { 598 queue = append(queue, u.UsedBy) 599 } 600 } 601 } 602 return nil 603 }, 604} 605 606var takeDownAccountCmd = &cli.Command{ 607 Name: "account-takedown", 608 Flags: []cli.Flag{ 609 &cli.StringFlag{ 610 Name: "reason", 611 Usage: "why the account is being taken down", 612 Required: true, 613 }, 614 &cli.StringFlag{ 615 Name: "admin-user", 616 Usage: "account of person running this command, for recordkeeping", 617 Required: true, 618 }, 619 }, 620 Action: func(cctx *cli.Context) error { 621 622 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 623 if err != nil { 624 return err 625 } 626 627 ctx := context.Background() 628 629 adminKey := cctx.String("admin-password") 630 xrpcc.AdminToken = &adminKey 631 632 for _, did := range cctx.Args().Slice() { 633 if !strings.HasPrefix(did, "did:") { 634 dir := identity.DefaultDirectory() 635 resp, err := dir.LookupHandle(ctx, syntax.Handle(did)) 636 if err != nil { 637 return err 638 } 639 640 did = resp.DID.String() 641 } 642 643 reason := cctx.String("reason") 644 adminUser := cctx.String("admin-user") 645 if !strings.HasPrefix(adminUser, "did:") { 646 dir := identity.DefaultDirectory() 647 resp, err := dir.LookupHandle(ctx, syntax.Handle(adminUser)) 648 if err != nil { 649 return err 650 } 651 652 adminUser = resp.DID.String() 653 } 654 655 resp, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ 656 CreatedBy: adminUser, 657 Event: &toolsozone.ModerationEmitEvent_Input_Event{ 658 ModerationDefs_ModEventTakedown: &toolsozone.ModerationDefs_ModEventTakedown{ 659 Comment: &reason, 660 }, 661 }, 662 Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ 663 AdminDefs_RepoRef: &atproto.AdminDefs_RepoRef{ 664 Did: did, 665 }, 666 }, 667 }) 668 if err != nil { 669 return err 670 } 671 672 b, err := json.MarshalIndent(resp, "", " ") 673 if err != nil { 674 return err 675 } 676 677 fmt.Println(string(b)) 678 } 679 return nil 680 }, 681} 682 683var queryModerationStatusesCmd = &cli.Command{ 684 Name: "query-moderation-statuses", 685 ArgsUsage: "<did-or-handle>", 686 Action: func(cctx *cli.Context) error { 687 688 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 689 if err != nil { 690 return err 691 } 692 693 ctx := context.Background() 694 695 adminKey := cctx.String("admin-password") 696 xrpcc.AdminToken = &adminKey 697 698 did := cctx.Args().First() 699 if !strings.HasPrefix(did, "did:") { 700 phr := &handles.ProdHandleResolver{} 701 resp, err := phr.ResolveHandleToDid(ctx, did) 702 if err != nil { 703 return err 704 } 705 706 did = resp 707 } 708 709 resp, err := toolsozone.ModerationQueryEvents( 710 ctx, 711 xrpcc, 712 nil, // addedLabels []string 713 nil, // addedTags []string 714 "", // ageAssuranceState 715 nil, // collections []string 716 "", // comment string 717 "", // createdAfter string 718 "", // createdBefore string 719 "", // createdBy string 720 "", // cursor string 721 false, // hasComment bool 722 false, // includeAllUserRecords bool 723 100, // limit int64 724 nil, // modTool 725 nil, // policies []string 726 nil, // removedLabels []string 727 nil, // removedTags []string 728 nil, // reportTypes []string 729 "", // sortDirection string 730 "", // subject string 731 "", // subjectType string 732 []string{"tools.ozone.moderation.defs#modEventReport"}, // types []string 733 ) 734 if err != nil { 735 return err 736 } 737 738 b, err := json.MarshalIndent(resp, "", " ") 739 if err != nil { 740 return err 741 } 742 743 fmt.Println(string(b)) 744 return nil 745 }, 746} 747 748var createInviteCmd = &cli.Command{ 749 Name: "create-invites", 750 Flags: []cli.Flag{ 751 &cli.IntFlag{ 752 Name: "useCount", 753 Value: 1, 754 }, 755 &cli.IntFlag{ 756 Name: "num", 757 Value: 1, 758 }, 759 &cli.StringFlag{ 760 Name: "bulk", 761 }, 762 }, 763 ArgsUsage: "[handle]", 764 Action: func(cctx *cli.Context) error { 765 xrpcc, err := cliutil.GetXrpcClient(cctx, false) 766 if err != nil { 767 return err 768 } 769 770 adminKey := cctx.String("admin-password") 771 772 count := cctx.Int("useCount") 773 num := cctx.Int("num") 774 775 phr := &handles.ProdHandleResolver{} 776 if bulkfi := cctx.String("bulk"); bulkfi != "" { 777 xrpcc.AdminToken = &adminKey 778 dids, err := readDids(bulkfi) 779 if err != nil { 780 return err 781 } 782 783 for i, d := range dids { 784 if !strings.HasPrefix(d, "did:plc:") { 785 out, err := phr.ResolveHandleToDid(context.TODO(), d) 786 if err != nil { 787 return fmt.Errorf("failed to resolve %q: %w", d, err) 788 } 789 790 dids[i] = out 791 } 792 } 793 794 for n := 0; n < len(dids); n += 500 { 795 slice := dids 796 if len(slice) > 500 { 797 slice = slice[:500] 798 } 799 800 _, err = comatproto.ServerCreateInviteCodes(context.TODO(), xrpcc, &comatproto.ServerCreateInviteCodes_Input{ 801 UseCount: int64(count), 802 ForAccounts: slice, 803 CodeCount: int64(num), 804 }) 805 if err != nil { 806 return err 807 } 808 } 809 810 return nil 811 } 812 813 var usrdid []string 814 if forUser := cctx.Args().Get(0); forUser != "" { 815 if !strings.HasPrefix(forUser, "did:") { 816 resp, err := phr.ResolveHandleToDid(context.TODO(), forUser) 817 if err != nil { 818 return fmt.Errorf("resolving handle: %w", err) 819 } 820 821 usrdid = []string{resp} 822 } else { 823 usrdid = []string{forUser} 824 } 825 } 826 827 xrpcc.AdminToken = &adminKey 828 829 resp, err := comatproto.ServerCreateInviteCodes(context.TODO(), xrpcc, &comatproto.ServerCreateInviteCodes_Input{ 830 UseCount: int64(count), 831 ForAccounts: usrdid, 832 CodeCount: int64(num), 833 }) 834 if err != nil { 835 return fmt.Errorf("creating codes: %w", err) 836 } 837 838 for _, c := range resp.Codes { 839 for _, cc := range c.Codes { 840 fmt.Println(cc) 841 } 842 } 843 844 return nil 845 }, 846}