cleanup relay-compare script

This commit is contained in:
Evan Jarrett
2026-02-27 20:14:26 -06:00
parent 7c064ba8b0
commit 0827219716
+131 -40
View File
@@ -8,8 +8,10 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"sort"
@@ -17,7 +19,9 @@ import (
"sync"
"time"
"atcr.io/pkg/atproto"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/xrpc"
)
// ANSI color codes (disabled via --no-color or NO_COLOR env)
@@ -37,16 +41,16 @@ func disableColors() {
// All io.atcr.* collections to compare
var allCollections = []string{
atproto.ManifestCollection, // io.atcr.manifest
atproto.TagCollection, // io.atcr.tag
atproto.SailorProfileCollection, // io.atcr.sailor.profile
atproto.StarCollection, // io.atcr.sailor.star
atproto.RepoPageCollection, // io.atcr.repo.page
atproto.CaptainCollection, // io.atcr.hold.captain
atproto.CrewCollection, // io.atcr.hold.crew
atproto.LayerCollection, // io.atcr.hold.layer
atproto.StatsCollection, // io.atcr.hold.stats
atproto.ScanCollection, // io.atcr.hold.scan
"io.atcr.manifest",
"io.atcr.tag",
"io.atcr.sailor.profile",
"io.atcr.sailor.star",
"io.atcr.repo.page",
"io.atcr.hold.captain",
"io.atcr.hold.crew",
"io.atcr.hold.layer",
"io.atcr.hold.stats",
"io.atcr.hold.scan",
}
type summaryRow struct {
@@ -76,9 +80,29 @@ type diffEntry struct {
relayIdx int
}
// XRPC response types for listReposByCollection
type listReposByCollectionResult struct {
Repos []repoRef `json:"repos"`
Cursor string `json:"cursor,omitempty"`
}
type repoRef struct {
DID string `json:"did"`
}
// XRPC response types for listRecords
type listRecordsResult struct {
Records []json.RawMessage `json:"records"`
Cursor string `json:"cursor,omitempty"`
}
// Shared identity directory for DID resolution
var dir identity.Directory
func main() {
noColor := flag.Bool("no-color", false, "disable colored output")
verify := flag.Bool("verify", false, "verify diffs against PDS to distinguish real gaps from ghost entries")
hideGhosts := flag.Bool("hide-ghosts", false, "with --verify, hide ghost and deactivated entries from output")
collection := flag.String("collection", "", "compare only this collection")
timeout := flag.Duration("timeout", 2*time.Minute, "timeout for all relay queries")
flag.Usage = func() {
@@ -113,6 +137,8 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
dir = identity.DefaultDirectory()
// Short display names for each relay
names := make([]string, len(relays))
maxNameLen := 0
@@ -253,6 +279,7 @@ func main() {
fmt.Printf("\n %sMissing from %s (%d):%s\n", cRed, names[ri], len(missing), cReset)
for _, did := range missing {
suffix := ""
skip := false
if *verify {
vr, ok := verified[key{col, did}]
if !ok {
@@ -263,6 +290,7 @@ func main() {
suffix = fmt.Sprintf(" %s← deactivated%s", cDim, cReset)
row.deactivated++
totalDeactivated++
skip = *hideGhosts
} else if vr.exists {
suffix = fmt.Sprintf(" %s← real gap%s", cRed, cReset)
row.realGaps++
@@ -271,9 +299,12 @@ func main() {
suffix = fmt.Sprintf(" %s← ghost (not on PDS)%s", cDim, cReset)
row.ghosts++
totalGhosts++
skip = *hideGhosts
}
}
fmt.Printf(" %s- %s%s%s\n", cRed, did, cReset, suffix)
if !skip {
fmt.Printf(" %s- %s%s%s\n", cRed, did, cReset, suffix)
}
}
}
@@ -283,7 +314,10 @@ func main() {
}
if inSync {
notes := formatSyncNotes(row.ghosts, row.deactivated)
notes := ""
if !*hideGhosts {
notes = formatSyncNotes(row.ghosts, row.deactivated)
}
if notes != "" {
fmt.Printf(" %s✓ in sync%s %s(%s)%s\n", cGreen, cReset, cDim, notes, cReset)
} else {
@@ -297,28 +331,38 @@ func main() {
}
// Summary table
printSummary(summary, names, maxNameLen, totalMissing, *verify, totalRealGaps, totalGhosts, totalDeactivated)
printSummary(summary, names, maxNameLen, totalMissing, *verify, *hideGhosts, totalRealGaps, totalGhosts, totalDeactivated)
}
func printSummary(rows []summaryRow, names []string, maxNameLen, totalMissing int, showVerify bool, totalRealGaps, totalGhosts, totalDeactivated int) {
func printSummary(rows []summaryRow, names []string, maxNameLen, totalMissing int, showVerify, hideGhosts bool, totalRealGaps, totalGhosts, totalDeactivated int) {
fmt.Printf("\n%s%s━━━ Summary ━━━%s\n\n", cBold, cCyan, cReset)
colW := 28
relayW := maxNameLen + 2
if relayW < 8 {
relayW = 8
// Build short labels (A, B, C, ...) for compact columns
labels := make([]string, len(names))
for i, name := range names {
labels[i] = string(rune('A' + i))
fmt.Printf(" %s%s%s: %s\n", cBold, labels[i], cReset, name)
}
fmt.Println()
colW := len("Collection")
for _, row := range rows {
if len(row.collection) > colW {
colW = len(row.collection)
}
}
relayW := 6
// Header
fmt.Printf(" %-*s", colW, "Collection")
for _, name := range names {
fmt.Printf(" %*s", relayW, name)
for _, label := range labels {
fmt.Printf(" %*s", relayW, label)
}
fmt.Printf(" Status\n")
// Separator
fmt.Printf(" %s", strings.Repeat("─", colW))
for range names {
for range labels {
fmt.Printf(" %s", strings.Repeat("─", relayW))
}
fmt.Printf(" %s\n", strings.Repeat("─", 14))
@@ -336,7 +380,10 @@ func printSummary(rows []summaryRow, names []string, maxNameLen, totalMissing in
}
switch row.status {
case "sync":
notes := formatSyncNotes(row.ghosts, row.deactivated)
notes := ""
if !hideGhosts {
notes = formatSyncNotes(row.ghosts, row.deactivated)
}
if notes != "" {
fmt.Printf(" %s✓ in sync%s %s(%s)%s", cGreen, cReset, cDim, notes, cReset)
} else {
@@ -344,12 +391,16 @@ func printSummary(rows []summaryRow, names []string, maxNameLen, totalMissing in
}
case "diff":
if showVerify {
notes := formatSyncNotes(row.ghosts, row.deactivated)
if notes != "" {
notes = ", " + notes
if hideGhosts {
fmt.Printf(" %s≠ %d missing%s", cYellow, row.realGaps, cReset)
} else {
notes := formatSyncNotes(row.ghosts, row.deactivated)
if notes != "" {
notes = ", " + notes
}
fmt.Printf(" %s≠ %d missing%s %s(%d real%s)%s",
cYellow, row.realGaps, cReset, cDim, row.realGaps, notes, cReset)
}
fmt.Printf(" %s≠ %d missing%s %s(%s)%s",
cYellow, row.realGaps, cReset, cDim, fmt.Sprintf("%d real%s", row.realGaps, notes), cReset)
} else {
fmt.Printf(" %s≠ %d missing%s", cYellow, row.diffCount, cReset)
}
@@ -363,14 +414,20 @@ func printSummary(rows []summaryRow, names []string, maxNameLen, totalMissing in
fmt.Println()
if totalMissing > 0 {
if showVerify && totalRealGaps == 0 {
notes := formatSyncNotes(totalGhosts, totalDeactivated)
fmt.Printf("%s✓ All relays in sync%s %s(%s)%s\n", cGreen, cReset, cDim, notes, cReset)
if hideGhosts {
fmt.Printf("%s✓ All relays in sync%s\n", cGreen, cReset)
} else {
notes := formatSyncNotes(totalGhosts, totalDeactivated)
fmt.Printf("%s✓ All relays in sync%s %s(%s)%s\n", cGreen, cReset, cDim, notes, cReset)
}
} else {
if showVerify {
fmt.Printf("%s%d real gaps across relays%s", cYellow, totalRealGaps, cReset)
notes := formatSyncNotes(totalGhosts, totalDeactivated)
if notes != "" {
fmt.Printf(" %s(%s)%s", cDim, notes, cReset)
if !hideGhosts {
notes := formatSyncNotes(totalGhosts, totalDeactivated)
if notes != "" {
fmt.Printf(" %s(%s)%s", cDim, notes, cReset)
}
}
fmt.Println()
} else {
@@ -425,7 +482,7 @@ func verifyDiffs(ctx context.Context, diffs []diffEntry) map[key]verifyResult {
sem <- struct{}{}
defer func() { <-sem }()
pds, err := atproto.ResolveDIDToPDS(ctx, did)
pds, err := resolveDIDToPDS(ctx, did)
mu.Lock()
if err != nil {
pdsErrors[did] = err
@@ -466,8 +523,13 @@ func verifyDiffs(ctx context.Context, diffs []diffEntry) map[key]verifyResult {
}
pds := pdsEndpoints[dc.did]
client := atproto.NewClient(pds, "", "")
records, _, err := client.ListRecordsForRepo(ctx, dc.did, dc.col, 1, "")
client := &xrpc.Client{Host: pds, Client: http.DefaultClient}
var listResult listRecordsResult
err := client.LexDo(ctx, "GET", "", "com.atproto.repo.listRecords", map[string]any{
"repo": dc.did,
"collection": dc.col,
"limit": 1,
}, nil, &listResult)
mu.Lock()
if err != nil {
errStr := err.Error()
@@ -480,7 +542,7 @@ func verifyDiffs(ctx context.Context, diffs []diffEntry) map[key]verifyResult {
results[k] = verifyResult{err: err}
}
} else {
results[k] = verifyResult{exists: len(records) > 0}
results[k] = verifyResult{exists: len(listResult.Records) > 0}
}
mu.Unlock()
}(dc)
@@ -490,16 +552,45 @@ func verifyDiffs(ctx context.Context, diffs []diffEntry) map[key]verifyResult {
return results
}
// resolveDIDToPDS resolves a DID to its PDS endpoint using the shared identity directory.
func resolveDIDToPDS(ctx context.Context, did string) (string, error) {
didParsed, err := syntax.ParseDID(did)
if err != nil {
return "", fmt.Errorf("invalid DID: %w", err)
}
ident, err := dir.LookupDID(ctx, didParsed)
if err != nil {
return "", fmt.Errorf("failed to resolve DID: %w", err)
}
pdsEndpoint := ident.PDSEndpoint()
if pdsEndpoint == "" {
return "", fmt.Errorf("no PDS endpoint found for DID")
}
return pdsEndpoint, nil
}
// fetchAllDIDs paginates through listReposByCollection to collect all DIDs.
func fetchAllDIDs(ctx context.Context, relay, collection string) (map[string]struct{}, error) {
client := atproto.NewClient(relay, "", "")
client := &xrpc.Client{Host: relay, Client: http.DefaultClient}
dids := make(map[string]struct{})
var cursor string
for {
result, err := client.ListReposByCollection(ctx, collection, 1000, cursor)
params := map[string]any{
"collection": collection,
"limit": 1000,
}
if cursor != "" {
params["cursor"] = cursor
}
var result listReposByCollectionResult
err := client.LexDo(ctx, "GET", "", "com.atproto.sync.listReposByCollection", params, nil, &result)
if err != nil {
return dids, err
return dids, fmt.Errorf("listReposByCollection failed: %w", err)
}
for _, repo := range result.Repos {