this repo has no description

initial implementation that consumes tangled issue/comments and stores them in a db

willdot.net 623a43a3

+596
+2
.dockerignore
··· 1 + database.db 2 + tangled-alert-bot
+4
.gitignore
··· 1 + .env 2 + database.db 3 + tangled-alert-bot 4 + makefile
+17
Dockerfile
··· 1 + FROM golang:latest AS builder 2 + 3 + WORKDIR /app 4 + 5 + COPY . . 6 + RUN go mod download 7 + 8 + RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -a -installsuffix cgo -o tangled-alert-bot . 9 + 10 + FROM alpine:latest 11 + 12 + RUN apk --no-cache add ca-certificates 13 + 14 + WORKDIR /root/ 15 + COPY --from=builder /app/tangled-alert-bot . 16 + 17 + CMD ["./tangled-alert-bot"]
+87
cmd/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "log" 8 + "log/slog" 9 + "os" 10 + "os/signal" 11 + "path" 12 + "syscall" 13 + 14 + tangledalertbot "tangled.sh/willdot.net/tangled-alert-bot" 15 + 16 + "github.com/avast/retry-go/v4" 17 + "github.com/joho/godotenv" 18 + ) 19 + 20 + const ( 21 + defaultJetstreamAddr = "wss://jetstream.atproto.tools/subscribe" 22 + ) 23 + 24 + func main() { 25 + err := run() 26 + if err != nil { 27 + log.Fatal(err) 28 + } 29 + } 30 + 31 + func run() error { 32 + err := godotenv.Load() 33 + if err != nil && !os.IsNotExist(err) { 34 + return fmt.Errorf("error loading .env file") 35 + } 36 + 37 + signals := make(chan os.Signal, 1) 38 + signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) 39 + 40 + dbPath := os.Getenv("DATABASE_PATH") 41 + if dbPath == "" { 42 + dbPath = "./" 43 + } 44 + 45 + dbFilename := path.Join(dbPath, "database.db") 46 + database, err := tangledalertbot.NewDatabase(dbFilename) 47 + if err != nil { 48 + return fmt.Errorf("create new store: %w", err) 49 + } 50 + defer database.Close() 51 + 52 + ctx, cancel := context.WithCancel(context.Background()) 53 + defer cancel() 54 + 55 + go consumeLoop(ctx, database) 56 + 57 + <-signals 58 + cancel() 59 + 60 + return nil 61 + } 62 + 63 + func consumeLoop(ctx context.Context, database *tangledalertbot.Database) { 64 + handler := tangledalertbot.NewFeedHandler(database) 65 + 66 + jsServerAddr := os.Getenv("JS_SERVER_ADDR") 67 + if jsServerAddr == "" { 68 + jsServerAddr = defaultJetstreamAddr 69 + } 70 + 71 + consumer := tangledalertbot.NewJetstreamConsumer(jsServerAddr, slog.Default(), handler) 72 + 73 + _ = retry.Do(func() error { 74 + err := consumer.Consume(ctx) 75 + if err != nil { 76 + // if the context has been cancelled then it's time to exit 77 + if errors.Is(err, context.Canceled) { 78 + return nil 79 + } 80 + slog.Error("consume loop", "error", err) 81 + return err 82 + } 83 + return nil 84 + }, retry.Attempts(0)) // retry indefinitly until context canceled 85 + 86 + slog.Warn("exiting consume loop") 87 + }
+194
consumer.go
··· 1 + package tangledalertbot 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + 7 + "fmt" 8 + "log/slog" 9 + "time" 10 + 11 + "github.com/bluesky-social/jetstream/pkg/client" 12 + "github.com/bluesky-social/jetstream/pkg/client/schedulers/sequential" 13 + "github.com/bluesky-social/jetstream/pkg/models" 14 + "tangled.sh/tangled.sh/core/api/tangled" 15 + ) 16 + 17 + type Issue struct { 18 + AuthorDID string `json:"authorDID"` 19 + RKey string `json:"rkey"` 20 + Title string `json:"title"` 21 + Body string `json:"body"` 22 + Repo string `json:"repo"` 23 + CreatedAt int64 `json:"createdAt"` 24 + } 25 + 26 + type Comment struct { 27 + AuthorDID string `json:"authorDID"` 28 + RKey string `json:"rkey"` 29 + Body string `json:"body"` 30 + Issue string `json:"issue" ` 31 + ReplyTo string `json:"replyTo"` 32 + CreatedAt int64 `json:"createdAt"` 33 + } 34 + 35 + type Store interface { 36 + CreateIssue(issue Issue) error 37 + CreateComment(comment Comment) error 38 + } 39 + 40 + // JetstreamConsumer is responsible for consuming from a jetstream instance 41 + type JetstreamConsumer struct { 42 + cfg *client.ClientConfig 43 + handler *Handler 44 + logger *slog.Logger 45 + } 46 + 47 + // NewJetstreamConsumer configures a new jetstream consumer. To run or start you should call the Consume function 48 + func NewJetstreamConsumer(jsAddr string, logger *slog.Logger, handler *Handler) *JetstreamConsumer { 49 + cfg := client.DefaultClientConfig() 50 + if jsAddr != "" { 51 + cfg.WebsocketURL = jsAddr 52 + } 53 + cfg.WantedCollections = []string{ 54 + tangled.RepoIssueNSID, 55 + tangled.RepoIssueCommentNSID, 56 + } 57 + cfg.WantedDids = []string{} 58 + 59 + return &JetstreamConsumer{ 60 + cfg: cfg, 61 + logger: logger, 62 + handler: handler, 63 + } 64 + } 65 + 66 + // Consume will connect to a Jetstream client and start to consume and handle messages from it 67 + func (c *JetstreamConsumer) Consume(ctx context.Context) error { 68 + scheduler := sequential.NewScheduler("jetstream", c.logger, c.handler.HandleEvent) 69 + defer scheduler.Shutdown() 70 + 71 + client, err := client.NewClient(c.cfg, c.logger, scheduler) 72 + if err != nil { 73 + return fmt.Errorf("failed to create client: %w", err) 74 + } 75 + 76 + cursor := time.Now().Add(1 * -time.Minute).UnixMicro() 77 + 78 + if err := client.ConnectAndRead(ctx, &cursor); err != nil { 79 + return fmt.Errorf("connect and read: %w", err) 80 + } 81 + 82 + slog.Info("stopping consume") 83 + return nil 84 + } 85 + 86 + // Handler is responsible for handling a message consumed from Jetstream 87 + type Handler struct { 88 + store Store 89 + } 90 + 91 + // NewFeedHandler returns a new handler 92 + func NewFeedHandler(store Store) *Handler { 93 + return &Handler{store: store} 94 + } 95 + 96 + // HandleEvent will handle an event based on the event's commit operation 97 + func (h *Handler) HandleEvent(ctx context.Context, event *models.Event) error { 98 + if event.Commit == nil { 99 + return nil 100 + } 101 + 102 + switch event.Commit.Operation { 103 + case models.CommitOperationCreate: 104 + return h.handleCreateEvent(ctx, event) 105 + // TODO: handle deletes too 106 + default: 107 + return nil 108 + } 109 + } 110 + 111 + func (h *Handler) handleCreateEvent(ctx context.Context, event *models.Event) error { 112 + switch event.Commit.Collection { 113 + case tangled.RepoIssueNSID: 114 + h.handleIssueEvent(ctx, event) 115 + case tangled.RepoIssueCommentNSID: 116 + h.handleIssueCommentEvent(ctx, event) 117 + default: 118 + slog.Info("create event was not for expected collection", "RKey", "did", event.Did, event.Commit.RKey, "collection", event.Commit.Collection) 119 + return nil 120 + } 121 + 122 + return nil 123 + } 124 + 125 + func (h *Handler) handleIssueEvent(ctx context.Context, event *models.Event) { 126 + var issue tangled.RepoIssue 127 + 128 + err := json.Unmarshal(event.Commit.Record, &issue) 129 + if err != nil { 130 + slog.Error("error unmarshalling event record to issue", "error", err) 131 + return 132 + } 133 + 134 + did := event.Did 135 + rkey := event.Commit.RKey 136 + 137 + createdAt, err := time.Parse(time.RFC3339, issue.CreatedAt) 138 + if err != nil { 139 + slog.Error("parsing createdAt time from issue", "error", err, "timestamp", issue.CreatedAt) 140 + createdAt = time.Now().UTC() 141 + } 142 + body := "" 143 + if issue.Body != nil { 144 + body = *&body 145 + } 146 + err = h.store.CreateIssue(Issue{ 147 + AuthorDID: did, 148 + RKey: rkey, 149 + Title: issue.Title, 150 + Body: body, 151 + CreatedAt: createdAt.UnixMilli(), 152 + Repo: issue.Repo, 153 + }) 154 + if err != nil { 155 + slog.Error("create issue", "error", err, "did", did, "rkey", rkey) 156 + return 157 + } 158 + slog.Info("created issue ", "value", issue, "did", did, "rkey", rkey) 159 + } 160 + 161 + func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Event) { 162 + var comment tangled.RepoIssueComment 163 + 164 + err := json.Unmarshal(event.Commit.Record, &comment) 165 + if err != nil { 166 + slog.Error("error unmarshalling event record to comment", "error", err) 167 + return 168 + } 169 + 170 + did := event.Did 171 + rkey := event.Commit.RKey 172 + 173 + createdAt, err := time.Parse(time.RFC3339, comment.CreatedAt) 174 + if err != nil { 175 + slog.Error("parsing createdAt time from comment", "error", err, "timestamp", comment.CreatedAt) 176 + createdAt = time.Now().UTC() 177 + } 178 + err = h.store.CreateComment(Comment{ 179 + AuthorDID: did, 180 + RKey: rkey, 181 + Body: comment.Body, 182 + Issue: comment.Issue, 183 + CreatedAt: createdAt.UnixMilli(), 184 + //ReplyTo: comment, // TODO: there should be a ReplyTo field that can be used as well once the right type is imported 185 + }) 186 + if err != nil { 187 + slog.Error("create comment", "error", err, "did", did, "rkey", rkey) 188 + return 189 + } 190 + 191 + // TODO: now send a notification to either the issue creator or whoever the comment was a reply to 192 + 193 + slog.Info("created comment ", "value", comment, "did", did, "rkey", rkey) 194 + }
+144
database.go
··· 1 + package tangledalertbot 2 + 3 + import ( 4 + "database/sql" 5 + "errors" 6 + "fmt" 7 + "log/slog" 8 + "os" 9 + 10 + _ "github.com/glebarez/go-sqlite" 11 + ) 12 + 13 + // Database is a sqlite database 14 + type Database struct { 15 + db *sql.DB 16 + } 17 + 18 + // NewDatabase will open a new database. It will ping the database to ensure it is available and error if not 19 + func NewDatabase(dbPath string) (*Database, error) { 20 + if dbPath != ":memory:" { 21 + err := createDbFile(dbPath) 22 + if err != nil { 23 + return nil, fmt.Errorf("create db file: %w", err) 24 + } 25 + } 26 + 27 + db, err := sql.Open("sqlite", dbPath) 28 + if err != nil { 29 + return nil, fmt.Errorf("open database: %w", err) 30 + } 31 + 32 + err = db.Ping() 33 + if err != nil { 34 + return nil, fmt.Errorf("ping db: %w", err) 35 + } 36 + 37 + err = createIssuesTable(db) 38 + if err != nil { 39 + return nil, fmt.Errorf("creating issues table: %w", err) 40 + } 41 + 42 + err = createCommentsTable(db) 43 + if err != nil { 44 + return nil, fmt.Errorf("creating comments table: %w", err) 45 + } 46 + 47 + return &Database{db: db}, nil 48 + } 49 + 50 + // Close will cleanly stop the database connection 51 + func (d *Database) Close() { 52 + err := d.db.Close() 53 + if err != nil { 54 + slog.Error("failed to close db", "error", err) 55 + } 56 + } 57 + 58 + func createDbFile(dbFilename string) error { 59 + if _, err := os.Stat(dbFilename); !errors.Is(err, os.ErrNotExist) { 60 + return nil 61 + } 62 + 63 + f, err := os.Create(dbFilename) 64 + if err != nil { 65 + return fmt.Errorf("create db file : %w", err) 66 + } 67 + err = f.Close() 68 + if err != nil { 69 + return fmt.Errorf("failed to close DB file: %w", err) 70 + } 71 + return nil 72 + } 73 + 74 + func createIssuesTable(db *sql.DB) error { 75 + createTableSQL := `CREATE TABLE IF NOT EXISTS issues ( 76 + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 77 + "authorDid" TEXT, 78 + "rkey" TEXT, 79 + "title" TEXT, 80 + "body" TEXT, 81 + "repo" TEXT, 82 + "createdAt" integer NOT NULL, 83 + UNIQUE(authorDid,rkey) 84 + );` 85 + 86 + slog.Info("Create issues table...") 87 + statement, err := db.Prepare(createTableSQL) 88 + if err != nil { 89 + return fmt.Errorf("prepare DB statement to create issues table: %w", err) 90 + } 91 + _, err = statement.Exec() 92 + if err != nil { 93 + return fmt.Errorf("exec sql statement to create issues table: %w", err) 94 + } 95 + slog.Info("issues table created") 96 + 97 + return nil 98 + } 99 + 100 + func createCommentsTable(db *sql.DB) error { 101 + createTableSQL := `CREATE TABLE IF NOT EXISTS comments ( 102 + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 103 + "authorDid" TEXT, 104 + "rkey" TEXT, 105 + "body" TEXT, 106 + "issue" TEXT, 107 + "replyTo" TEXT, 108 + "createdAt" integer NOT NULL, 109 + UNIQUE(authorDid,rkey) 110 + );` 111 + 112 + slog.Info("Create comments table...") 113 + statement, err := db.Prepare(createTableSQL) 114 + if err != nil { 115 + return fmt.Errorf("prepare DB statement to create comments table: %w", err) 116 + } 117 + _, err = statement.Exec() 118 + if err != nil { 119 + return fmt.Errorf("exec sql statement to create comments table: %w", err) 120 + } 121 + slog.Info("comments table created") 122 + 123 + return nil 124 + } 125 + 126 + // CreateIssue will insert a issue into a database 127 + func (d *Database) CreateIssue(issue Issue) error { 128 + sql := `INSERT INTO issues (authorDid, rkey, title, body, repo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` 129 + _, err := d.db.Exec(sql, issue.AuthorDID, issue.RKey, issue.Title, issue.Body, issue.Repo, issue.CreatedAt) 130 + if err != nil { 131 + return fmt.Errorf("exec insert issue: %w", err) 132 + } 133 + return nil 134 + } 135 + 136 + // CreateComment will insert a comment into a database 137 + func (d *Database) CreateComment(comment Comment) error { 138 + sql := `INSERT INTO comments (authorDid, rkey, body, issue, replyTo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` 139 + _, err := d.db.Exec(sql, comment.AuthorDID, comment.RKey, comment.Body, comment.Issue, comment.ReplyTo, comment.CreatedAt) 140 + if err != nil { 141 + return fmt.Errorf("exec insert comment: %w", err) 142 + } 143 + return nil 144 + }
+51
go.mod
··· 1 + module tangled.sh/willdot.net/tangled-alert-bot 2 + 3 + go 1.25.0 4 + 5 + require ( 6 + github.com/avast/retry-go/v4 v4.6.1 7 + github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336 8 + github.com/glebarez/go-sqlite v1.22.0 9 + github.com/joho/godotenv v1.5.1 10 + tangled.sh/tangled.sh/core v1.8.1-alpha 11 + ) 12 + 13 + require ( 14 + github.com/beorn7/perks v1.0.1 // indirect 15 + github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b // indirect 16 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 + github.com/dustin/go-humanize v1.0.1 // indirect 18 + github.com/goccy/go-json v0.10.5 // indirect 19 + github.com/google/uuid v1.6.0 // indirect 20 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 21 + github.com/ipfs/go-cid v0.5.0 // indirect 22 + github.com/klauspost/compress v1.18.0 // indirect 23 + github.com/klauspost/cpuid/v2 v2.3.0 // indirect 24 + github.com/mattn/go-isatty v0.0.20 // indirect 25 + github.com/minio/sha256-simd v1.0.1 // indirect 26 + github.com/mr-tron/base58 v1.2.0 // indirect 27 + github.com/multiformats/go-base32 v0.1.0 // indirect 28 + github.com/multiformats/go-base36 v0.2.0 // indirect 29 + github.com/multiformats/go-multibase v0.2.0 // indirect 30 + github.com/multiformats/go-multihash v0.2.3 // indirect 31 + github.com/multiformats/go-varint v0.0.7 // indirect 32 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 33 + github.com/prometheus/client_golang v1.22.0 // indirect 34 + github.com/prometheus/client_model v0.6.2 // indirect 35 + github.com/prometheus/common v0.64.0 // indirect 36 + github.com/prometheus/procfs v0.16.1 // indirect 37 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 38 + github.com/spaolacci/murmur3 v1.1.0 // indirect 39 + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect 40 + go.uber.org/atomic v1.11.0 // indirect 41 + golang.org/x/crypto v0.40.0 // indirect 42 + golang.org/x/net v0.42.0 // indirect 43 + golang.org/x/sys v0.34.0 // indirect 44 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 45 + google.golang.org/protobuf v1.36.6 // indirect 46 + lukechampine.com/blake3 v1.4.1 // indirect 47 + modernc.org/libc v1.37.6 // indirect 48 + modernc.org/mathutil v1.6.0 // indirect 49 + modernc.org/memory v1.7.2 // indirect 50 + modernc.org/sqlite v1.28.0 // indirect 51 + )
+97
go.sum
··· 1 + github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= 2 + github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= 3 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 + github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b h1:bJTlFwMhB9sluuqZxVXtv2yFcaWOC/PZokz9mcwb4u4= 6 + github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q= 7 + github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336 h1:NM3wfeFUrdjCE/xHLXQorwQvEKlI9uqnWl7L0Y9KA8U= 8 + github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336/go.mod h1:3ihWQCbXeayg41G8lQ5DfB/3NnEhl0XX24eZ3mLpf7Q= 9 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 14 + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 15 + github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= 16 + github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= 17 + github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 18 + github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 19 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 20 + github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 21 + github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 22 + github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 23 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 24 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 26 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 27 + github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 28 + github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 29 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 30 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 31 + github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 32 + github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 33 + github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 34 + github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 35 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 36 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 37 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 38 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 39 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 40 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 41 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 42 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 43 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 44 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 45 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 46 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 47 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 48 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 49 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 50 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 51 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 52 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 53 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 54 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 + github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 56 + github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 57 + github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 58 + github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 59 + github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 60 + github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 61 + github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 62 + github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 63 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 64 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 65 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 66 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 67 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 68 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 69 + github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 70 + github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 71 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 72 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 73 + golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= 74 + golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 75 + golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 76 + golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 77 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 + golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 79 + golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 80 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 81 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 82 + google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 83 + google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 84 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 + lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= 87 + lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= 88 + modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= 89 + modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= 90 + modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 91 + modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 92 + modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= 93 + modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= 94 + modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= 95 + modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= 96 + tangled.sh/tangled.sh/core v1.8.1-alpha h1:mCBXOUfzNCT1AnbMnaBrc/AgvfnxOIf5rSIescecpko= 97 + tangled.sh/tangled.sh/core v1.8.1-alpha/go.mod h1:9kSVXCu9DMszZoQ5P4Rgdpz+RHLMjbHy++53qE7EBoU=