From ccd50988a21247be441dc8d4a052510acdfcb76c Mon Sep 17 00:00:00 2001 From: "whey.party" Date: Sat, 6 Dec 2025 16:13:56 +0100 Subject: [PATCH] backstream init --- backstream/atproto.go | 297 ++++++++++++++ backstream/handler.go | 547 +++++++++++++++++++++++++ backstream/session.go | 113 +++++ main.go => cmd/appview/main.go | 5 - utils.go => cmd/appview/utils.go | 0 cmd/backstream/jetstream-zstd-dict.bin | Bin 0 -> 112640 bytes cmd/backstream/main.go | 77 ++++ cmd/jetrelay/main.go | 26 ++ go.mod | 40 +- go.sum | 227 +++++++++- readme.md | 46 ++- 11 files changed, 1354 insertions(+), 24 deletions(-) create mode 100644 backstream/atproto.go create mode 100644 backstream/handler.go create mode 100644 backstream/session.go rename main.go => cmd/appview/main.go (99%) rename utils.go => cmd/appview/utils.go (100%) create mode 100644 cmd/backstream/jetstream-zstd-dict.bin create mode 100644 cmd/backstream/main.go create mode 100644 cmd/jetrelay/main.go diff --git a/backstream/atproto.go b/backstream/atproto.go new file mode 100644 index 0000000..e4584ae --- /dev/null +++ b/backstream/atproto.go @@ -0,0 +1,297 @@ +package backstream + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +type ATProtoClient struct { + HTTPClient *http.Client + RelayHost string + PLCHost string + didCache map[string]string + didCacheLock sync.RWMutex +} + +type ListReposResponse struct { + Cursor string `json:"cursor"` + Repos []RepoInfo `json:"repos"` +} +type RepoInfo struct { + DID string `json:"did"` + Head string `json:"head"` +} + +type ListRecordsResponse struct { + Cursor string `json:"cursor"` + Records []Record `json:"records"` +} +type Record struct { + URI string `json:"uri"` + CID string `json:"cid"` + Value interface{} `json:"value"` +} + +type GetRecordOutput struct { + URI string `json:"uri"` + CID string `json:"cid"` + Value interface{} `json:"value"` +} + +type JetstreamLikeOutput struct { + Did string `json:"did"` + Kind string `json:"kind"` + TimeUS json.Number `json:"time_us"` + Commit JetstreamLikeCommit `json:"commit"` +} + +type JetstreamLikeCommit struct { + Rev string `json:"rev"` + Operation string `json:"operation"` + Collection string `json:"collection"` + RKey string `json:"rkey"` + Record interface{} `json:"record"` + CID string `json:"cid"` +} + +func NewATProtoClient(relayHost, plcHost string) *ATProtoClient { + return &ATProtoClient{ + HTTPClient: &http.Client{Timeout: 30 * time.Second}, + RelayHost: relayHost, + PLCHost: plcHost, + didCache: make(map[string]string), + } +} + +func (c *ATProtoClient) ListRepos(ctx context.Context, cursor string) ([]RepoInfo, string, error) { + u, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.listRepos", c.RelayHost)) + q := u.Query() + q.Set("limit", "1000") + if cursor != "" { + q.Set("cursor", cursor) + } + u.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + if err != nil { + return nil, "", err + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("listRepos non-200 status: %s", resp.Status) + } + + var data ListReposResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, "", err + } + + return data.Repos, data.Cursor, nil +} + +func (c *ATProtoClient) ListReposByCollection(ctx context.Context, collection, cursor string) ([]RepoInfo, string, error) { + u, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.listReposByCollection", c.RelayHost)) + q := u.Query() + q.Set("collection", collection) + q.Set("limit", "500") + if cursor != "" { + q.Set("cursor", cursor) + } + u.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + if err != nil { + return nil, "", err + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("listReposByCollection non-200 status: %s", resp.Status) + } + + var data ListReposResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, "", err + } + + return data.Repos, data.Cursor, nil +} + +func (c *ATProtoClient) ListRecords(ctx context.Context, pdsURL, repo, collection, cursor string) ([]Record, string, error) { + u, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", pdsURL)) + q := u.Query() + q.Set("repo", repo) + q.Set("collection", collection) + q.Set("limit", "100") + if cursor != "" { + q.Set("cursor", cursor) + } + u.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + if err != nil { + return nil, "", err + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, "", fmt.Errorf("listRecords non-200 status for %s: %s body: %s", repo, resp.Status, string(body)) + } + + var data ListRecordsResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, "", err + } + + return data.Records, data.Cursor, nil +} + +type didDoc struct { + Service []struct { + ID string `json:"id"` + Type string `json:"type"` + ServiceEndpoint string `json:"serviceEndpoint"` + } `json:"service"` +} + +func (c *ATProtoClient) ResolveDID(ctx context.Context, did string) (string, error) { + c.didCacheLock.RLock() + cachedURL, found := c.didCache[did] + c.didCacheLock.RUnlock() + if found { + return cachedURL, nil + } + + var doc didDoc + var err error + + if strings.HasPrefix(did, "did:plc:") { + doc, err = c.resolvePLC(ctx, did) + } else if strings.HasPrefix(did, "did:web:") { + doc, err = c.resolveWeb(ctx, did) + } else { + return "", fmt.Errorf("unsupported DID method for: %s", did) + } + + if err != nil { + return "", err + } + + for _, s := range doc.Service { + if s.ID == "#atproto_pds" { + c.didCacheLock.Lock() + c.didCache[did] = s.ServiceEndpoint + c.didCacheLock.Unlock() + return s.ServiceEndpoint, nil + } + } + + return "", fmt.Errorf("PDS service endpoint not found in DID document for %s", did) +} + +func (c *ATProtoClient) resolvePLC(ctx context.Context, did string) (didDoc, error) { + u := fmt.Sprintf("%s/%s", c.PLCHost, did) + return c.fetchDIDDoc(ctx, u) +} + +func (c *ATProtoClient) resolveWeb(ctx context.Context, did string) (didDoc, error) { + domain := strings.TrimPrefix(did, "did:web:") + u := fmt.Sprintf("https://%s/.well-known/did.json", domain) + return c.fetchDIDDoc(ctx, u) +} + +func (c *ATProtoClient) fetchDIDDoc(ctx context.Context, url string) (didDoc, error) { + var doc didDoc + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return doc, err + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return doc, fmt.Errorf("failed to fetch DID doc from %s: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return doc, fmt.Errorf("bad status from DID doc fetch (%s): %s", url, resp.Status) + } + + if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { + return doc, fmt.Errorf("failed to parse DID doc from %s: %w", url, err) + } + return doc, nil +} + +type DescribeRepoResponse struct { + Collections []string `json:"collections"` +} + +func (c *ATProtoClient) DescribeRepo(ctx context.Context, pdsURL, did string) ([]string, error) { + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.describeRepo?repo=%s", pdsURL, did) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + if err != nil { + return nil, err + } + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("describeRepo non-200 status for %s: %s body: %s", did, resp.Status, string(body)) + } + var data DescribeRepoResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, err + } + return data.Collections, nil +} + +func (c *ATProtoClient) GetRepo(ctx context.Context, pdsURL, did string) (io.ReadCloser, error) { + client := &http.Client{Timeout: 600 * time.Second} + + u := fmt.Sprintf("%s/xrpc/com.atproto.sync.getRepo?did=%s", pdsURL, did) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("getRepo request failed for %s: %w", did, err) + } + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + return nil, fmt.Errorf("getRepo non-200 status for %s: %s body: %s", did, resp.Status, string(body)) + } + + return resp.Body, nil +} diff --git a/backstream/handler.go b/backstream/handler.go new file mode 100644 index 0000000..bf92868 --- /dev/null +++ b/backstream/handler.go @@ -0,0 +1,547 @@ +package backstream + +import ( + //"bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "strings" + "sync" + "time" + + "io" + "io/ioutil" + "os" + + "runtime" + "runtime/debug" + + "github.com/gorilla/websocket" + "github.com/klauspost/compress/zstd" + + data "github.com/bluesky-social/indigo/atproto/atdata" + atrepo "github.com/bluesky-social/indigo/atproto/repo" + + // "github.com/bluesky-social/indigo/repo" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/ipfs/go-cid" +) + +const ( + numWorkers = 20 +) + +var DefaultUpgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +type BackfillHandler struct { + Upgrader websocket.Upgrader + SessionManager *SessionManager + AtpClient *ATProtoClient + ZstdDict []byte + UseGetRepoMethod bool +} + +type BackfillParams struct { + WantedDIDs []string + WantedCollections []string + GetRecordFormat bool +} + +func (h *BackfillHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + compress := (r.URL.Query().Get("compress") == "true") && (h.ZstdDict != nil) + + conn, err := h.Upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Failed to upgrade connection: %v", err) + return + } + defer conn.Close() + + if compress { + log.Println("Client requested zstd compression. Enabling.") + } + + params, ticket, err := h.parseQueryParams(r) + if err != nil { + h.sendError(conn, err.Error()) + return + } + + log.Printf("New connection for ticket: %s. DIDs: %v, Collections: %v, Workers: %d", ticket, params.WantedDIDs, params.WantedCollections, numWorkers) + + session := h.SessionManager.GetOrCreate(ticket, params) + session.LastAccessed = time.Now() + + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + + go func() { + defer cancel() + for { + if _, _, err := conn.ReadMessage(); err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("Client disconnected for ticket %s (read error): %v", ticket, err) + } + break + } + } + }() + + var wg sync.WaitGroup + jobs := make(chan string, numWorkers) + results := make(chan interface{}, 100) + + for i := 1; i <= numWorkers; i++ { + go h.worker(ctx, i, &wg, jobs, results, session) + } + + writerDone := make(chan struct{}) + if compress { + go h.compressedWriter(ctx, cancel, conn, results, writerDone) + } else { + go h.writer(ctx, cancel, conn, results, writerDone) + } + + wg.Add(1) + go h.producer(ctx, &wg, jobs, session) + + wg.Wait() + close(results) + <-writerDone + + log.Printf("Backfill completed for ticket: %s", session.Ticket) + h.sendMessage(conn, map[string]string{"status": "complete", "message": "Backfill finished."}) +} + +func (h *BackfillHandler) compressedWriter(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, results <-chan interface{}, done chan<- struct{}) { + defer close(done) + + encoder, err := zstd.NewWriter(nil, zstd.WithEncoderDict(h.ZstdDict)) + if err != nil { + log.Printf("ERROR: [CompressedWriter] Failed to create zstd encoder with dictionary: %v", err) + cancel() + return + } + defer encoder.Close() + + for { + select { + case result, ok := <-results: + if !ok { + return + } + + data, err := json.Marshal(result) + if err != nil { + log.Printf("ERROR: [CompressedWriter] Failed to marshal JSON: %v", err) + cancel() + return + } + + compressed := encoder.EncodeAll(data, nil) + + if err := conn.WriteMessage(websocket.BinaryMessage, compressed); err != nil { + log.Printf("ERROR: [CompressedWriter] Failed to write compressed message: %v", err) + cancel() + return + } + case <-ctx.Done(): + log.Printf("[CompressedWriter] Context cancelled, stopping.") + return + } + } +} + +func (h *BackfillHandler) writer(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, results <-chan interface{}, done chan<- struct{}) { + defer close(done) + for { + select { + case result, ok := <-results: + if !ok { + return + } + if err := h.sendMessage(conn, result); err != nil { + log.Printf("ERROR: [Writer] Failed to write message, closing connection: %v", err) + cancel() + return + } + case <-ctx.Done(): + log.Printf("[Writer] Context cancelled, stopping.") + return + } + } +} + +func (h *BackfillHandler) producer(ctx context.Context, wg *sync.WaitGroup, jobs chan<- string, session *Session) { + defer close(jobs) + defer wg.Done() + + isFullNetwork := len(session.Params.WantedDIDs) == 1 && session.Params.WantedDIDs[0] == "*" + isAllCollections := len(session.Params.WantedCollections) == 1 && session.Params.WantedCollections[0] == "*" + + if isFullNetwork { + if isAllCollections { + // --- Case 1: Full Network, All Collections (dids=*&collections=*) --- + // We need to list *all* repos from the relay. + log.Printf("[Producer] Starting full network scan for all collections.") + for { + select { + case <-ctx.Done(): + log.Printf("[Producer] Context cancelled, stopping full repo fetch.") + return + default: + } + + log.Printf("[Producer] Fetching all repos with cursor: %s", session.ListReposCursor) + repos, nextCursor, err := h.AtpClient.ListRepos(ctx, session.ListReposCursor) + if err != nil { + log.Printf("ERROR: [Producer] Failed to list all repos: %v", err) + return + } + + for _, repo := range repos { + if !session.IsDIDComplete(repo.DID) { + wg.Add(1) + jobs <- repo.DID + } + } + + session.mu.Lock() + session.ListReposCursor = nextCursor + session.LastAccessed = time.Now() + session.mu.Unlock() + + if nextCursor == "" { + log.Printf("[Producer] Finished fetching all repos from relay.") + break + } + } + } else { + // --- Case 2: Full Network, Specific Collections (dids=*&collections=a,b,c) --- + // For each specific collection, page through all repos and send DIDs to workers. + log.Printf("[Producer] Starting network scan for specific collections: %v", session.Params.WantedCollections) + for _, collection := range session.Params.WantedCollections { + for { + select { + case <-ctx.Done(): + log.Printf("[Producer] Context cancelled, stopping repo fetch.") + return + default: + } + + log.Printf("[Producer] Fetching repos for %s with cursor: %s", collection, session.ListReposCursor) + repos, nextCursor, err := h.AtpClient.ListReposByCollection(ctx, collection, session.ListReposCursor) + if err != nil { + log.Printf("ERROR: [Producer] Failed to list repos for collection %s: %v", collection, err) + return + } + + for _, repo := range repos { + if !session.IsDIDComplete(repo.DID) { + wg.Add(1) + jobs <- repo.DID + } + } + + session.mu.Lock() + session.ListReposCursor = nextCursor + session.LastAccessed = time.Now() + session.mu.Unlock() + + if nextCursor == "" { + log.Printf("[Producer] Finished fetching all repos for collection %s", collection) + break + } + } + } + } + } else { + // --- Case 3: Specific List of DIDs (dids=a,b,c) --- + // Send user-provided DIDs to workers. + for _, did := range session.Params.WantedDIDs { + select { + case <-ctx.Done(): + log.Printf("[Producer] Context cancelled, stopping DID processing.") + return + default: + if !session.IsDIDComplete(did) { + wg.Add(1) + jobs <- did + } else { + log.Printf("[Producer] Skipping already completed DID: %s", did) + } + } + } + } +} + +func (h *BackfillHandler) worker(ctx context.Context, id int, wg *sync.WaitGroup, jobs <-chan string, results chan<- interface{}, session *Session) { + for did := range jobs { + func(did string) { + defer func() { + wg.Done() + + runtime.GC() + debug.FreeOSMemory() + + log.Printf("[Worker %d] Cleaned up resources for DID: %s", id, did) + }() + + select { + case <-ctx.Done(): + return + default: + } + + log.Printf("[Worker %d] Processing DID: %s", id, did) + pdsURL, err := h.AtpClient.ResolveDID(ctx, did) + if err != nil { + log.Printf("WARN: [Worker %d] Could not resolve DID %s, skipping. Error: %v", id, did, err) + return + } + + if h.UseGetRepoMethod { + h.processDIDWithGetRepo(ctx, id, did, pdsURL, results, session) + } else { + h.processDIDWithListRecords(ctx, id, did, pdsURL, results, session) + } + + session.MarkDIDComplete(did) + log.Printf("[Worker %d] Finished DID: %s", id, did) + }(did) + } +} + +func (h *BackfillHandler) processDIDWithGetRepo(ctx context.Context, id int, did, pdsURL string, results chan<- interface{}, session *Session) { + log.Printf("[Worker %d] Using streaming getRepo method for %s", id, did) + isAllCollections := len(session.Params.WantedCollections) == 1 && session.Params.WantedCollections[0] == "*" + + wantedSet := make(map[string]struct{}) + if !isAllCollections { + for _, coll := range session.Params.WantedCollections { + wantedSet[coll] = struct{}{} + } + } + + respBody, err := h.AtpClient.GetRepo(ctx, pdsURL, did) + if err != nil { + log.Printf("WARN: [Worker %d] Failed to get repo stream for %s: %v", id, did, err) + return + } + defer respBody.Close() + + if err := os.MkdirAll("./temp", 0o755); err != nil { + panic(err) + } + tempFile, err := ioutil.TempFile("./temp", "backstream-repo-*.car") + if err != nil { + log.Printf("ERROR: [Worker %d] Failed to create temp file for %s: %v", id, did, err) + return + } + defer os.Remove(tempFile.Name()) + + if _, err := io.Copy(tempFile, respBody); err != nil { + log.Printf("ERROR: [Worker %d] Failed to write repo to temp file for %s: %v", id, did, err) + return + } + + if err := tempFile.Close(); err != nil { + log.Printf("ERROR: [Worker %d] Failed to close temp file for %s: %v", id, did, err) + return + } + + readHandle, err := os.Open(tempFile.Name()) + if err != nil { + log.Printf("ERROR: [Worker %d] Failed to open temp file for reading %s: %v", id, did, err) + return + } + defer readHandle.Close() + + _, r, err := atrepo.LoadRepoFromCAR(ctx, readHandle) + if err != nil { + log.Printf("WARN: [Worker %d] Failed to read CAR stream for %s from temp file: %v", id, did, err) + return + } + + err = r.MST.Walk(func(k []byte, v cid.Cid) error { + select { + case <-ctx.Done(): + return errors.New("context cancelled during repo walk") + default: + } + + path := string(k) + collection, rkey, err := syntax.ParseRepoPath(path) + if err != nil { + log.Printf("WARN: [Worker %d] Could not parse repo path '%s' for %s, skipping record", id, path, did) + return nil + } + + if !isAllCollections { + if _, ok := wantedSet[string(collection)]; !ok { + return nil + } + } + + recBytes, _, err := r.GetRecordBytes(ctx, collection, rkey) + if err != nil { + log.Printf("WARN: [Worker %d] Failed to get record bytes for %s: %v", id, path, err) + return nil + } + + recordVal, err := data.UnmarshalCBOR(recBytes) + if err != nil { + log.Printf("WARN: [Worker %d] Failed to unmarshal record CBOR for %s: %v", id, path, err) + return nil + } + + record := Record{ + URI: fmt.Sprintf("at://%s/%s", did, path), + CID: v.String(), + Value: recordVal, + } + + output := h.formatOutput(record, did, string(collection), session.Params.GetRecordFormat) + select { + case results <- output: + case <-ctx.Done(): + return errors.New("context cancelled while sending result") + } + + session.SetListRecordsCursor(did, string(collection), string(rkey)) + return nil + }) + + if err != nil && !errors.Is(err, context.Canceled) { + log.Printf("WARN: [Worker %d] Error while walking repo for %s: %v", id, did, err) + } +} + +func (h *BackfillHandler) processDIDWithListRecords(ctx context.Context, id int, did, pdsURL string, results chan<- interface{}, session *Session) { + log.Printf("[Worker %d] Using listRecords method for %s", id, did) + isAllCollections := len(session.Params.WantedCollections) == 1 && session.Params.WantedCollections[0] == "*" + var collectionsToProcess []string + + if isAllCollections { + repoCollections, err := h.AtpClient.DescribeRepo(ctx, pdsURL, did) + if err != nil { + log.Printf("WARN: [Worker %d] Could not describe repo for %s to find collections, skipping. Error: %v", id, did, err) + return + } + collectionsToProcess = repoCollections + log.Printf("[Worker %d] Found %d collections for DID %s", id, len(collectionsToProcess), did) + } else { + collectionsToProcess = session.Params.WantedCollections + } + + for _, collection := range collectionsToProcess { + cursor := session.GetListRecordsCursor(did, collection) + for { + select { + case <-ctx.Done(): + log.Printf("[Worker %d] Context cancelled for DID %s", id, did) + return + default: + } + + records, nextCursor, err := h.AtpClient.ListRecords(ctx, pdsURL, did, collection, cursor) + if err != nil { + if !strings.Contains(err.Error(), "status: 400") { + log.Printf("WARN: [Worker %d] Failed to list records for %s/%s, skipping. Error: %v", id, did, collection, err) + } + break + } + + for _, record := range records { + output := h.formatOutput(record, did, collection, session.Params.GetRecordFormat) + select { + case results <- output: + case <-ctx.Done(): + log.Printf("[Worker %d] Context cancelled while sending results for %s", id, did) + return + } + } + + session.SetListRecordsCursor(did, collection, nextCursor) + cursor = nextCursor + if cursor == "" { + break + } + } + } +} + +func (h *BackfillHandler) parseQueryParams(r *http.Request) (BackfillParams, string, error) { + query := r.URL.Query() + ticket := query.Get("ticket") + + wantedDidsStr := query.Get("wantedDids") + wantedCollectionsStr := query.Get("wantedCollections") + + if wantedCollectionsStr == "" && wantedDidsStr == "" && ticket == "" { + ticket = "jetstreamfalse" + } else if ticket == "" { + ticket = generateTicket() + } + + if wantedDidsStr == "" { + log.Println("Query parameter 'wantedDids' not specified, defaulting to '*' (all repos).") + wantedDidsStr = "*" + } + + if wantedCollectionsStr == "" { + log.Println("Query parameter 'wantedCollections' not specified, defaulting to '*' (all collections).") + wantedCollectionsStr = "*" + } + + params := BackfillParams{ + WantedDIDs: strings.Split(wantedDidsStr, ","), + WantedCollections: strings.Split(wantedCollectionsStr, ","), + GetRecordFormat: query.Get("getRecordFormat") == "true", + } + return params, ticket, nil +} + +func (h *BackfillHandler) formatOutput(record Record, did, collection string, getRecordFormat bool) interface{} { + if getRecordFormat { + return GetRecordOutput{ + URI: record.URI, + CID: record.CID, + Value: record.Value, + } + } + uriParts := strings.Split(record.URI, "/") + rkey := "" + if len(uriParts) == 5 { + rkey = uriParts[4] + } + return JetstreamLikeOutput{ + Did: did, + Kind: "commit", + TimeUS: "1725911162329308", + Commit: JetstreamLikeCommit{ + Rev: rkey, + Operation: "create", + Collection: collection, + RKey: rkey, + Record: record.Value, + CID: record.CID, + }, + } +} + +func (h *BackfillHandler) sendError(conn *websocket.Conn, message string) { + log.Printf("Sending error to client: %s", message) + _ = conn.WriteJSON(map[string]string{"error": message}) +} + +func (h *BackfillHandler) sendMessage(conn *websocket.Conn, v interface{}) error { + return conn.WriteJSON(v) +} diff --git a/backstream/session.go b/backstream/session.go new file mode 100644 index 0000000..09ba6eb --- /dev/null +++ b/backstream/session.go @@ -0,0 +1,113 @@ +package backstream + +import ( + "crypto/rand" + "encoding/hex" + "log" + "sync" + "time" +) + +type Session struct { + Ticket string + Params BackfillParams + LastAccessed time.Time + + ListReposCursor string // Cursor for listReposByCollection if wantedDids=* + CompletedDIDs map[string]bool // Set of DIDs that have been fully processed. + listRecordsCursors map[string]string // Key: "did/collection", Value: cursor + + mu sync.Mutex +} + +func (s *Session) GetListRecordsCursor(did, collection string) string { + s.mu.Lock() + defer s.mu.Unlock() + key := did + "/" + collection + return s.listRecordsCursors[key] +} + +func (s *Session) SetListRecordsCursor(did, collection, cursor string) { + s.mu.Lock() + defer s.mu.Unlock() + key := did + "/" + collection + s.listRecordsCursors[key] = cursor + s.LastAccessed = time.Now() +} + +func (s *Session) MarkDIDComplete(did string) { + s.mu.Lock() + defer s.mu.Unlock() + s.CompletedDIDs[did] = true + s.LastAccessed = time.Now() +} + +func (s *Session) IsDIDComplete(did string) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.CompletedDIDs[did] +} + +type SessionManager struct { + sessions map[string]*Session + ttl time.Duration + mu sync.Mutex +} + +func NewSessionManager(ttl time.Duration) *SessionManager { + sm := &SessionManager{ + sessions: make(map[string]*Session), + ttl: ttl, + } + go sm.cleanupLoop() + return sm +} + +func (sm *SessionManager) GetOrCreate(ticket string, params BackfillParams) *Session { + sm.mu.Lock() + defer sm.mu.Unlock() + + if session, exists := sm.sessions[ticket]; exists { + log.Printf("Resuming existing session for ticket: %s", ticket) + session.LastAccessed = time.Now() + if session.CompletedDIDs == nil { + session.CompletedDIDs = make(map[string]bool) + } + return session + } + + log.Printf("Creating new session for ticket: %s", ticket) + newSession := &Session{ + Ticket: ticket, + Params: params, + LastAccessed: time.Now(), + listRecordsCursors: make(map[string]string), + CompletedDIDs: make(map[string]bool), + } + sm.sessions[ticket] = newSession + return newSession +} + +func (sm *SessionManager) cleanupLoop() { + ticker := time.NewTicker(sm.ttl / 2) + defer ticker.Stop() + for range ticker.C { + sm.mu.Lock() + now := time.Now() + for ticket, session := range sm.sessions { + if now.Sub(session.LastAccessed) > sm.ttl { + log.Printf("Session %s expired. Cleaning up.", ticket) + delete(sm.sessions, ticket) + } + } + sm.mu.Unlock() + } +} + +func generateTicket() string { + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + return "fallback-ticket-" + time.Now().String() + } + return hex.EncodeToString(bytes) +} diff --git a/main.go b/cmd/appview/main.go similarity index 99% rename from main.go rename to cmd/appview/main.go index a66ef1b..62d8405 100644 --- a/main.go +++ b/cmd/appview/main.go @@ -510,11 +510,6 @@ func main() { return } - if err != nil { - c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)}) - return - } - var targetEndpoint string for _, svc := range didDoc.Service { if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") { diff --git a/utils.go b/cmd/appview/utils.go similarity index 100% rename from utils.go rename to cmd/appview/utils.go diff --git a/cmd/backstream/jetstream-zstd-dict.bin b/cmd/backstream/jetstream-zstd-dict.bin new file mode 100644 index 0000000000000000000000000000000000000000..106847e7007b4ac409fd58912fed85d69aedb352 GIT binary patch literal 112640 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 NGcW*;mlJ#98UX)HUhe<^ literal 0 HcmV?d00001 diff --git a/cmd/backstream/main.go b/cmd/backstream/main.go new file mode 100644 index 0000000..edd0dc4 --- /dev/null +++ b/cmd/backstream/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "runtime/debug" + "time" + + "tangled.org/whey.party/red-dwarf-server/backstream" + + _ "net/http/pprof" +) + +const ( + defaultRelay = "https://relay1.us-west.bsky.network" + + plcDirectory = "https://plc.directory" + + zstdDictionaryPath = "jetstream-zstd-dict.bin" + + useGetRepoMethod = true +) + +func rootHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + fmt.Fprint(w, `Welcome to Backstream`) +} + +func main() { + debug.SetMemoryLimit(2 << 30) // 2 GB + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + log.Println("Starting Bluesky Backfill Service...") + if useGetRepoMethod { + log.Println("INFO: Using efficient 'getRepo' CAR file method for backfills.") + } else { + log.Println("INFO: Using 'listRecords' pagination method for backfills.") + } + + zstdDict, err := os.ReadFile(zstdDictionaryPath) + if err != nil { + log.Printf("WARN: Could not read zstd dictionary file '%s': %v", zstdDictionaryPath, err) + log.Println("WARN: Zstd compression will be unavailable.") + } else { + log.Printf("Successfully loaded zstd dictionary (%d bytes).", len(zstdDict)) + } + + sessionManager := backstream.NewSessionManager(5 * time.Minute) + + atpClient := backstream.NewATProtoClient(defaultRelay, plcDirectory) + + backfillHandler := &backstream.BackfillHandler{ + Upgrader: backstream.DefaultUpgrader, + SessionManager: sessionManager, + AtpClient: atpClient, + ZstdDict: zstdDict, + UseGetRepoMethod: useGetRepoMethod, + } + + http.Handle("/subscribe", backfillHandler) + + http.HandleFunc("/", rootHandler) + + log.Println("Server listening on :3877") + log.Println("Connect via WebSocket to: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=*") + log.Println("---") + log.Println("Example with specific DIDs: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=did:plc:abc,did:plc:xyz") + log.Println("Example with ticket for resumable session: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=*&ticket=my-session-123") + log.Println("Example with alternative output format: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=*&getRecordFormat=true") + + if err := http.ListenAndServe(":3877", nil); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/cmd/jetrelay/main.go b/cmd/jetrelay/main.go new file mode 100644 index 0000000..f116887 --- /dev/null +++ b/cmd/jetrelay/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + "fmt" +) + +type multiFlag []string + +func (m *multiFlag) String() string { + return fmt.Sprint(*m) +} + +func (m *multiFlag) Set(value string) error { + *m = append(*m, value) + return nil +} + +func main() { + var js multiFlag + flag.Var(&js, "j", "jetstream instances 'write multiple to input more than one'") + + flag.Parse() + + fmt.Println(js) // prints: [hi hello what] +} diff --git a/go.mod b/go.mod index 2f4be8e..20c2bd2 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,13 @@ go 1.25.4 require ( github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 + github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/golang-lru/arc/v2 v2.0.7 + github.com/klauspost/compress v1.18.2 github.com/prometheus/client_golang v1.23.2 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 @@ -18,15 +20,41 @@ require ( ) require ( - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/gin-contrib/cors v1.7.6 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/ipfs/bbloom v0.0.4 // indirect + github.com/ipfs/go-block-format v0.2.0 // indirect + github.com/ipfs/go-blockservice v0.5.2 // indirect + github.com/ipfs/go-datastore v0.6.0 // indirect + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect + github.com/ipfs/go-ipfs-util v0.0.3 // indirect + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect + github.com/ipfs/go-ipld-format v0.6.0 // indirect + github.com/ipfs/go-ipld-legacy v0.2.1 // indirect + github.com/ipfs/go-log v1.0.5 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/ipfs/go-merkledag v0.11.0 // indirect + github.com/ipfs/go-metrics-interface v0.0.1 // indirect + github.com/ipfs/go-verifcid v0.0.3 // indirect + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 // indirect + github.com/ipld/go-codec-dagpb v1.6.0 // indirect + github.com/ipld/go-ipld-prime v0.21.0 // indirect + github.com/jbenet/goprocess v0.1.4 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect github.com/segmentio/asm v1.2.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect ) require ( @@ -47,7 +75,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-cid v0.5.0 github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -71,7 +99,7 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -87,7 +115,7 @@ require ( golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.35.0 // indirect - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.9 // indirect - lukechampine.com/blake3 v1.2.1 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 1ea5647..a56bea3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc h1:2t+uAvfzJiCsTMwn5fW85t/IGa0+2I7BXS2ORastK4o= @@ -10,20 +14,24 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= +github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 h1:puGwrNTY2vCt8eakkSEq2yeNxUD3zb2kPhv1OsF1hPs= github.com/ericvolp12/jwt-go-secp256k1 v0.0.2/go.mod h1:ntxzdN7EhBp8h+N78AtN2hjbVKHa7mijryYd9nPMyMo= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -45,33 +53,115 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= -github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= +github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= +github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= +github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= +github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= +github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= +github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= +github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= +github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= +github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= +github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= +github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= +github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= +github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= +github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= +github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= +github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= +github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= +github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= +github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= +github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= +github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= +github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= +github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY= +github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= +github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= +github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= +github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= +github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= +github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= +github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= +github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= +github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= +github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= +github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= +github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= +github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= +github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= +github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU= +github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM= +github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= +github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= +github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 h1:oFo19cBmcP0Cmg3XXbrr0V/c+xU9U1huEZp8+OgBzdI= +github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4/go.mod h1:6nkFF8OmR5wLKBzRKi7/YFJpyYR7+oEn1DX+mMWnlLA= +github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= +github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= +github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= +github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= +github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= +github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= +github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -89,8 +179,33 @@ github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= +github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= +github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= +github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= +github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= +github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= +github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= +github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= +github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= +github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= +github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= +github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= +github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -104,18 +219,33 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc= +github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= +github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= +github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o= +github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= +github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -128,17 +258,28 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -149,10 +290,18 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= -github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= +github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 h1:W4jEGWdes35iuiiAYNZFOjx+dwzQOBh33kVpc0C0YiE= github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= @@ -172,38 +321,73 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -229,21 +413,40 @@ golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= -lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/readme.md b/readme.md index ecbf4f6..d20197f 100644 --- a/readme.md +++ b/readme.md @@ -15,4 +15,48 @@ implemented routes: - `app.bsky.feed.getPosts` (post rendering is incomplete) - `app.bsky.feed.getFeed` (post rendering is incomplete) - `app.bsky.unspecced.getConfig` (placeholder) -- `app.bsky.unspecced.getPostThreadV2` (mostly working! doesnt use prefered sort, not performant) \ No newline at end of file +- `app.bsky.unspecced.getPostThreadV2` (mostly working! doesnt use prefered sort, not performant yet) + +> [!NOTE] +> uh im not very confident with the current directory structure, so files and folders might move around + +## Runnables +run all of these using `go run .` inside the respective directories + +### `/cmd/appview` +the main entry point, the actual appview itself. the api server that implements app.bsky.* XRPC methods + +### `/cmd/backstream` +experimental backfiller that kinda (but not really) conforms to the jetstream event shape. designed to be ingested by consumers expecting jetstream + +## Packages + +### `/auth` +taken from [go-bsky-feed-generator](https://github.com/jazware/go-bsky-feed-generator) but modified a bit. + +handles all of the auth, modified to have a more lenient version to make `getFeed` work + +### `/microcosm/*` +microcosm api clients, implements constellation slingshot and spacedust + +slingshot's api client is compatible with `github.com/bluesky-social/indigo/*` stuff, like `agnostic.RepoGetRecord` and `util.LexClient` + +### `/shims/*` +most of Red Dwarf Server logic lives here. pulls data from upstream services like microcosm constellation and slingshot, transforms it, and spits out bsky api -like responses using the published app.bsky.* codegen from `github.com/bluesky-social/indigo/api/bsky` + + +### `/sticket` +unused leftover sorry + + +### `/store` +unused leftover sorry + +## todo + +- appview-side query caches +- notification service +- bookmarks service +- create aturilist service +- make backstream usable +- create jetrelay service \ No newline at end of file -- 2.54.0