bluesky appview implementation using microcosm and other services server.reddwarf.app
appview bluesky reddwarf microcosm

Compare changes

Choose any two refs to compare.

Changed files
+5419 -102
auth
backstream
cmd
appview
aturilist
backstream
jetrelay
public
shims
lex
app
bsky
actor
feed
unspecced
getpostthreadv2
utils
sticket
store
+2
.gitignore
··· 1 + cmd/aturilist/badger_data 2 + cmd/backstream/temp
+35 -1
auth/auth.go
··· 200 200 } 201 201 202 202 if claims.Audience != auth.ServiceDID { 203 - c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("Invalid audience (expected %s)", auth.ServiceDID)}) 203 + c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("Invalid audience (found '%s', expected '%s')", claims.Audience, auth.ServiceDID)}) 204 204 c.Abort() 205 205 return 206 206 } ··· 211 211 span.End() 212 212 c.Next() 213 213 } 214 + 215 + func (auth *Auth) AuthenticateGinRequestViaJWTUnsafe(c *gin.Context) { 216 + tracer := otel.Tracer("auth") 217 + ctx, span := tracer.Start(c.Request.Context(), "Auth:AuthenticateGinRequestViaJWT") 218 + 219 + authHeader := c.GetHeader("Authorization") 220 + if authHeader == "" { 221 + span.End() 222 + c.Next() 223 + return 224 + } 225 + 226 + claims := jwt.StandardClaims{} 227 + 228 + err := auth.GetClaimsFromAuthHeader(ctx, authHeader, &claims) 229 + if err != nil { 230 + c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Errorf("Failed to get claims from auth header: %v", err).Error()}) 231 + span.End() 232 + c.Abort() 233 + return 234 + } 235 + 236 + // if claims.Audience != auth.ServiceDID { 237 + // c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("Invalid audience (found '%s', expected '%s')", claims.Audience, auth.ServiceDID)}) 238 + // c.Abort() 239 + // return 240 + // } 241 + 242 + // Set claims Issuer to context as user DID 243 + c.Set("user_did", claims.Issuer) 244 + span.SetAttributes(attribute.String("user.did", claims.Issuer)) 245 + span.End() 246 + c.Next() 247 + }
+297
backstream/atproto.go
··· 1 + package backstream 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "net/http" 9 + "net/url" 10 + "strings" 11 + "sync" 12 + "time" 13 + ) 14 + 15 + type ATProtoClient struct { 16 + HTTPClient *http.Client 17 + RelayHost string 18 + PLCHost string 19 + didCache map[string]string 20 + didCacheLock sync.RWMutex 21 + } 22 + 23 + type ListReposResponse struct { 24 + Cursor string `json:"cursor"` 25 + Repos []RepoInfo `json:"repos"` 26 + } 27 + type RepoInfo struct { 28 + DID string `json:"did"` 29 + Head string `json:"head"` 30 + } 31 + 32 + type ListRecordsResponse struct { 33 + Cursor string `json:"cursor"` 34 + Records []Record `json:"records"` 35 + } 36 + type Record struct { 37 + URI string `json:"uri"` 38 + CID string `json:"cid"` 39 + Value interface{} `json:"value"` 40 + } 41 + 42 + type GetRecordOutput struct { 43 + URI string `json:"uri"` 44 + CID string `json:"cid"` 45 + Value interface{} `json:"value"` 46 + } 47 + 48 + type JetstreamLikeOutput struct { 49 + Did string `json:"did"` 50 + Kind string `json:"kind"` 51 + TimeUS json.Number `json:"time_us"` 52 + Commit JetstreamLikeCommit `json:"commit"` 53 + } 54 + 55 + type JetstreamLikeCommit struct { 56 + Rev string `json:"rev"` 57 + Operation string `json:"operation"` 58 + Collection string `json:"collection"` 59 + RKey string `json:"rkey"` 60 + Record interface{} `json:"record"` 61 + CID string `json:"cid"` 62 + } 63 + 64 + func NewATProtoClient(relayHost, plcHost string) *ATProtoClient { 65 + return &ATProtoClient{ 66 + HTTPClient: &http.Client{Timeout: 30 * time.Second}, 67 + RelayHost: relayHost, 68 + PLCHost: plcHost, 69 + didCache: make(map[string]string), 70 + } 71 + } 72 + 73 + func (c *ATProtoClient) ListRepos(ctx context.Context, cursor string) ([]RepoInfo, string, error) { 74 + u, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.listRepos", c.RelayHost)) 75 + q := u.Query() 76 + q.Set("limit", "1000") 77 + if cursor != "" { 78 + q.Set("cursor", cursor) 79 + } 80 + u.RawQuery = q.Encode() 81 + 82 + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) 83 + if err != nil { 84 + return nil, "", err 85 + } 86 + 87 + resp, err := c.HTTPClient.Do(req) 88 + if err != nil { 89 + return nil, "", err 90 + } 91 + defer resp.Body.Close() 92 + 93 + if resp.StatusCode != http.StatusOK { 94 + return nil, "", fmt.Errorf("listRepos non-200 status: %s", resp.Status) 95 + } 96 + 97 + var data ListReposResponse 98 + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { 99 + return nil, "", err 100 + } 101 + 102 + return data.Repos, data.Cursor, nil 103 + } 104 + 105 + func (c *ATProtoClient) ListReposByCollection(ctx context.Context, collection, cursor string) ([]RepoInfo, string, error) { 106 + u, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.listReposByCollection", c.RelayHost)) 107 + q := u.Query() 108 + q.Set("collection", collection) 109 + q.Set("limit", "500") 110 + if cursor != "" { 111 + q.Set("cursor", cursor) 112 + } 113 + u.RawQuery = q.Encode() 114 + 115 + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) 116 + if err != nil { 117 + return nil, "", err 118 + } 119 + 120 + resp, err := c.HTTPClient.Do(req) 121 + if err != nil { 122 + return nil, "", err 123 + } 124 + defer resp.Body.Close() 125 + 126 + if resp.StatusCode != http.StatusOK { 127 + return nil, "", fmt.Errorf("listReposByCollection non-200 status: %s", resp.Status) 128 + } 129 + 130 + var data ListReposResponse 131 + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { 132 + return nil, "", err 133 + } 134 + 135 + return data.Repos, data.Cursor, nil 136 + } 137 + 138 + func (c *ATProtoClient) ListRecords(ctx context.Context, pdsURL, repo, collection, cursor string) ([]Record, string, error) { 139 + u, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", pdsURL)) 140 + q := u.Query() 141 + q.Set("repo", repo) 142 + q.Set("collection", collection) 143 + q.Set("limit", "100") 144 + if cursor != "" { 145 + q.Set("cursor", cursor) 146 + } 147 + u.RawQuery = q.Encode() 148 + 149 + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) 150 + if err != nil { 151 + return nil, "", err 152 + } 153 + 154 + resp, err := c.HTTPClient.Do(req) 155 + if err != nil { 156 + return nil, "", err 157 + } 158 + defer resp.Body.Close() 159 + 160 + if resp.StatusCode != http.StatusOK { 161 + body, _ := io.ReadAll(resp.Body) 162 + return nil, "", fmt.Errorf("listRecords non-200 status for %s: %s body: %s", repo, resp.Status, string(body)) 163 + } 164 + 165 + var data ListRecordsResponse 166 + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { 167 + return nil, "", err 168 + } 169 + 170 + return data.Records, data.Cursor, nil 171 + } 172 + 173 + type didDoc struct { 174 + Service []struct { 175 + ID string `json:"id"` 176 + Type string `json:"type"` 177 + ServiceEndpoint string `json:"serviceEndpoint"` 178 + } `json:"service"` 179 + } 180 + 181 + func (c *ATProtoClient) ResolveDID(ctx context.Context, did string) (string, error) { 182 + c.didCacheLock.RLock() 183 + cachedURL, found := c.didCache[did] 184 + c.didCacheLock.RUnlock() 185 + if found { 186 + return cachedURL, nil 187 + } 188 + 189 + var doc didDoc 190 + var err error 191 + 192 + if strings.HasPrefix(did, "did:plc:") { 193 + doc, err = c.resolvePLC(ctx, did) 194 + } else if strings.HasPrefix(did, "did:web:") { 195 + doc, err = c.resolveWeb(ctx, did) 196 + } else { 197 + return "", fmt.Errorf("unsupported DID method for: %s", did) 198 + } 199 + 200 + if err != nil { 201 + return "", err 202 + } 203 + 204 + for _, s := range doc.Service { 205 + if s.ID == "#atproto_pds" { 206 + c.didCacheLock.Lock() 207 + c.didCache[did] = s.ServiceEndpoint 208 + c.didCacheLock.Unlock() 209 + return s.ServiceEndpoint, nil 210 + } 211 + } 212 + 213 + return "", fmt.Errorf("PDS service endpoint not found in DID document for %s", did) 214 + } 215 + 216 + func (c *ATProtoClient) resolvePLC(ctx context.Context, did string) (didDoc, error) { 217 + u := fmt.Sprintf("%s/%s", c.PLCHost, did) 218 + return c.fetchDIDDoc(ctx, u) 219 + } 220 + 221 + func (c *ATProtoClient) resolveWeb(ctx context.Context, did string) (didDoc, error) { 222 + domain := strings.TrimPrefix(did, "did:web:") 223 + u := fmt.Sprintf("https://%s/.well-known/did.json", domain) 224 + return c.fetchDIDDoc(ctx, u) 225 + } 226 + 227 + func (c *ATProtoClient) fetchDIDDoc(ctx context.Context, url string) (didDoc, error) { 228 + var doc didDoc 229 + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 230 + if err != nil { 231 + return doc, err 232 + } 233 + 234 + resp, err := c.HTTPClient.Do(req) 235 + if err != nil { 236 + return doc, fmt.Errorf("failed to fetch DID doc from %s: %w", url, err) 237 + } 238 + defer resp.Body.Close() 239 + 240 + if resp.StatusCode != http.StatusOK { 241 + return doc, fmt.Errorf("bad status from DID doc fetch (%s): %s", url, resp.Status) 242 + } 243 + 244 + if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { 245 + return doc, fmt.Errorf("failed to parse DID doc from %s: %w", url, err) 246 + } 247 + return doc, nil 248 + } 249 + 250 + type DescribeRepoResponse struct { 251 + Collections []string `json:"collections"` 252 + } 253 + 254 + func (c *ATProtoClient) DescribeRepo(ctx context.Context, pdsURL, did string) ([]string, error) { 255 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.describeRepo?repo=%s", pdsURL, did) 256 + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) 257 + if err != nil { 258 + return nil, err 259 + } 260 + resp, err := c.HTTPClient.Do(req) 261 + if err != nil { 262 + return nil, err 263 + } 264 + defer resp.Body.Close() 265 + if resp.StatusCode != http.StatusOK { 266 + body, _ := io.ReadAll(resp.Body) 267 + return nil, fmt.Errorf("describeRepo non-200 status for %s: %s body: %s", did, resp.Status, string(body)) 268 + } 269 + var data DescribeRepoResponse 270 + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { 271 + return nil, err 272 + } 273 + return data.Collections, nil 274 + } 275 + 276 + func (c *ATProtoClient) GetRepo(ctx context.Context, pdsURL, did string) (io.ReadCloser, error) { 277 + client := &http.Client{Timeout: 600 * time.Second} 278 + 279 + u := fmt.Sprintf("%s/xrpc/com.atproto.sync.getRepo?did=%s", pdsURL, did) 280 + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) 281 + if err != nil { 282 + return nil, err 283 + } 284 + 285 + resp, err := client.Do(req) 286 + if err != nil { 287 + return nil, fmt.Errorf("getRepo request failed for %s: %w", did, err) 288 + } 289 + 290 + if resp.StatusCode != http.StatusOK { 291 + body, _ := io.ReadAll(resp.Body) 292 + resp.Body.Close() 293 + return nil, fmt.Errorf("getRepo non-200 status for %s: %s body: %s", did, resp.Status, string(body)) 294 + } 295 + 296 + return resp.Body, nil 297 + }
+547
backstream/handler.go
··· 1 + package backstream 2 + 3 + import ( 4 + //"bytes" 5 + "context" 6 + "encoding/json" 7 + "errors" 8 + "fmt" 9 + "log" 10 + "net/http" 11 + "strings" 12 + "sync" 13 + "time" 14 + 15 + "io" 16 + "io/ioutil" 17 + "os" 18 + 19 + "runtime" 20 + "runtime/debug" 21 + 22 + "github.com/gorilla/websocket" 23 + "github.com/klauspost/compress/zstd" 24 + 25 + data "github.com/bluesky-social/indigo/atproto/atdata" 26 + atrepo "github.com/bluesky-social/indigo/atproto/repo" 27 + 28 + // "github.com/bluesky-social/indigo/repo" 29 + "github.com/bluesky-social/indigo/atproto/syntax" 30 + "github.com/ipfs/go-cid" 31 + ) 32 + 33 + const ( 34 + numWorkers = 20 35 + ) 36 + 37 + var DefaultUpgrader = websocket.Upgrader{ 38 + CheckOrigin: func(r *http.Request) bool { 39 + return true 40 + }, 41 + } 42 + 43 + type BackfillHandler struct { 44 + Upgrader websocket.Upgrader 45 + SessionManager *SessionManager 46 + AtpClient *ATProtoClient 47 + ZstdDict []byte 48 + UseGetRepoMethod bool 49 + } 50 + 51 + type BackfillParams struct { 52 + WantedDIDs []string 53 + WantedCollections []string 54 + GetRecordFormat bool 55 + } 56 + 57 + func (h *BackfillHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 58 + compress := (r.URL.Query().Get("compress") == "true") && (h.ZstdDict != nil) 59 + 60 + conn, err := h.Upgrader.Upgrade(w, r, nil) 61 + if err != nil { 62 + log.Printf("Failed to upgrade connection: %v", err) 63 + return 64 + } 65 + defer conn.Close() 66 + 67 + if compress { 68 + log.Println("Client requested zstd compression. Enabling.") 69 + } 70 + 71 + params, ticket, err := h.parseQueryParams(r) 72 + if err != nil { 73 + h.sendError(conn, err.Error()) 74 + return 75 + } 76 + 77 + log.Printf("New connection for ticket: %s. DIDs: %v, Collections: %v, Workers: %d", ticket, params.WantedDIDs, params.WantedCollections, numWorkers) 78 + 79 + session := h.SessionManager.GetOrCreate(ticket, params) 80 + session.LastAccessed = time.Now() 81 + 82 + ctx, cancel := context.WithCancel(r.Context()) 83 + defer cancel() 84 + 85 + go func() { 86 + defer cancel() 87 + for { 88 + if _, _, err := conn.ReadMessage(); err != nil { 89 + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 90 + log.Printf("Client disconnected for ticket %s (read error): %v", ticket, err) 91 + } 92 + break 93 + } 94 + } 95 + }() 96 + 97 + var wg sync.WaitGroup 98 + jobs := make(chan string, numWorkers) 99 + results := make(chan interface{}, 100) 100 + 101 + for i := 1; i <= numWorkers; i++ { 102 + go h.worker(ctx, i, &wg, jobs, results, session) 103 + } 104 + 105 + writerDone := make(chan struct{}) 106 + if compress { 107 + go h.compressedWriter(ctx, cancel, conn, results, writerDone) 108 + } else { 109 + go h.writer(ctx, cancel, conn, results, writerDone) 110 + } 111 + 112 + wg.Add(1) 113 + go h.producer(ctx, &wg, jobs, session) 114 + 115 + wg.Wait() 116 + close(results) 117 + <-writerDone 118 + 119 + log.Printf("Backfill completed for ticket: %s", session.Ticket) 120 + h.sendMessage(conn, map[string]string{"status": "complete", "message": "Backfill finished."}) 121 + } 122 + 123 + func (h *BackfillHandler) compressedWriter(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, results <-chan interface{}, done chan<- struct{}) { 124 + defer close(done) 125 + 126 + encoder, err := zstd.NewWriter(nil, zstd.WithEncoderDict(h.ZstdDict)) 127 + if err != nil { 128 + log.Printf("ERROR: [CompressedWriter] Failed to create zstd encoder with dictionary: %v", err) 129 + cancel() 130 + return 131 + } 132 + defer encoder.Close() 133 + 134 + for { 135 + select { 136 + case result, ok := <-results: 137 + if !ok { 138 + return 139 + } 140 + 141 + data, err := json.Marshal(result) 142 + if err != nil { 143 + log.Printf("ERROR: [CompressedWriter] Failed to marshal JSON: %v", err) 144 + cancel() 145 + return 146 + } 147 + 148 + compressed := encoder.EncodeAll(data, nil) 149 + 150 + if err := conn.WriteMessage(websocket.BinaryMessage, compressed); err != nil { 151 + log.Printf("ERROR: [CompressedWriter] Failed to write compressed message: %v", err) 152 + cancel() 153 + return 154 + } 155 + case <-ctx.Done(): 156 + log.Printf("[CompressedWriter] Context cancelled, stopping.") 157 + return 158 + } 159 + } 160 + } 161 + 162 + func (h *BackfillHandler) writer(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, results <-chan interface{}, done chan<- struct{}) { 163 + defer close(done) 164 + for { 165 + select { 166 + case result, ok := <-results: 167 + if !ok { 168 + return 169 + } 170 + if err := h.sendMessage(conn, result); err != nil { 171 + log.Printf("ERROR: [Writer] Failed to write message, closing connection: %v", err) 172 + cancel() 173 + return 174 + } 175 + case <-ctx.Done(): 176 + log.Printf("[Writer] Context cancelled, stopping.") 177 + return 178 + } 179 + } 180 + } 181 + 182 + func (h *BackfillHandler) producer(ctx context.Context, wg *sync.WaitGroup, jobs chan<- string, session *Session) { 183 + defer close(jobs) 184 + defer wg.Done() 185 + 186 + isFullNetwork := len(session.Params.WantedDIDs) == 1 && session.Params.WantedDIDs[0] == "*" 187 + isAllCollections := len(session.Params.WantedCollections) == 1 && session.Params.WantedCollections[0] == "*" 188 + 189 + if isFullNetwork { 190 + if isAllCollections { 191 + // --- Case 1: Full Network, All Collections (dids=*&collections=*) --- 192 + // We need to list *all* repos from the relay. 193 + log.Printf("[Producer] Starting full network scan for all collections.") 194 + for { 195 + select { 196 + case <-ctx.Done(): 197 + log.Printf("[Producer] Context cancelled, stopping full repo fetch.") 198 + return 199 + default: 200 + } 201 + 202 + log.Printf("[Producer] Fetching all repos with cursor: %s", session.ListReposCursor) 203 + repos, nextCursor, err := h.AtpClient.ListRepos(ctx, session.ListReposCursor) 204 + if err != nil { 205 + log.Printf("ERROR: [Producer] Failed to list all repos: %v", err) 206 + return 207 + } 208 + 209 + for _, repo := range repos { 210 + if !session.IsDIDComplete(repo.DID) { 211 + wg.Add(1) 212 + jobs <- repo.DID 213 + } 214 + } 215 + 216 + session.mu.Lock() 217 + session.ListReposCursor = nextCursor 218 + session.LastAccessed = time.Now() 219 + session.mu.Unlock() 220 + 221 + if nextCursor == "" { 222 + log.Printf("[Producer] Finished fetching all repos from relay.") 223 + break 224 + } 225 + } 226 + } else { 227 + // --- Case 2: Full Network, Specific Collections (dids=*&collections=a,b,c) --- 228 + // For each specific collection, page through all repos and send DIDs to workers. 229 + log.Printf("[Producer] Starting network scan for specific collections: %v", session.Params.WantedCollections) 230 + for _, collection := range session.Params.WantedCollections { 231 + for { 232 + select { 233 + case <-ctx.Done(): 234 + log.Printf("[Producer] Context cancelled, stopping repo fetch.") 235 + return 236 + default: 237 + } 238 + 239 + log.Printf("[Producer] Fetching repos for %s with cursor: %s", collection, session.ListReposCursor) 240 + repos, nextCursor, err := h.AtpClient.ListReposByCollection(ctx, collection, session.ListReposCursor) 241 + if err != nil { 242 + log.Printf("ERROR: [Producer] Failed to list repos for collection %s: %v", collection, err) 243 + return 244 + } 245 + 246 + for _, repo := range repos { 247 + if !session.IsDIDComplete(repo.DID) { 248 + wg.Add(1) 249 + jobs <- repo.DID 250 + } 251 + } 252 + 253 + session.mu.Lock() 254 + session.ListReposCursor = nextCursor 255 + session.LastAccessed = time.Now() 256 + session.mu.Unlock() 257 + 258 + if nextCursor == "" { 259 + log.Printf("[Producer] Finished fetching all repos for collection %s", collection) 260 + break 261 + } 262 + } 263 + } 264 + } 265 + } else { 266 + // --- Case 3: Specific List of DIDs (dids=a,b,c) --- 267 + // Send user-provided DIDs to workers. 268 + for _, did := range session.Params.WantedDIDs { 269 + select { 270 + case <-ctx.Done(): 271 + log.Printf("[Producer] Context cancelled, stopping DID processing.") 272 + return 273 + default: 274 + if !session.IsDIDComplete(did) { 275 + wg.Add(1) 276 + jobs <- did 277 + } else { 278 + log.Printf("[Producer] Skipping already completed DID: %s", did) 279 + } 280 + } 281 + } 282 + } 283 + } 284 + 285 + func (h *BackfillHandler) worker(ctx context.Context, id int, wg *sync.WaitGroup, jobs <-chan string, results chan<- interface{}, session *Session) { 286 + for did := range jobs { 287 + func(did string) { 288 + defer func() { 289 + wg.Done() 290 + 291 + runtime.GC() 292 + debug.FreeOSMemory() 293 + 294 + log.Printf("[Worker %d] Cleaned up resources for DID: %s", id, did) 295 + }() 296 + 297 + select { 298 + case <-ctx.Done(): 299 + return 300 + default: 301 + } 302 + 303 + log.Printf("[Worker %d] Processing DID: %s", id, did) 304 + pdsURL, err := h.AtpClient.ResolveDID(ctx, did) 305 + if err != nil { 306 + log.Printf("WARN: [Worker %d] Could not resolve DID %s, skipping. Error: %v", id, did, err) 307 + return 308 + } 309 + 310 + if h.UseGetRepoMethod { 311 + h.processDIDWithGetRepo(ctx, id, did, pdsURL, results, session) 312 + } else { 313 + h.processDIDWithListRecords(ctx, id, did, pdsURL, results, session) 314 + } 315 + 316 + session.MarkDIDComplete(did) 317 + log.Printf("[Worker %d] Finished DID: %s", id, did) 318 + }(did) 319 + } 320 + } 321 + 322 + func (h *BackfillHandler) processDIDWithGetRepo(ctx context.Context, id int, did, pdsURL string, results chan<- interface{}, session *Session) { 323 + log.Printf("[Worker %d] Using streaming getRepo method for %s", id, did) 324 + isAllCollections := len(session.Params.WantedCollections) == 1 && session.Params.WantedCollections[0] == "*" 325 + 326 + wantedSet := make(map[string]struct{}) 327 + if !isAllCollections { 328 + for _, coll := range session.Params.WantedCollections { 329 + wantedSet[coll] = struct{}{} 330 + } 331 + } 332 + 333 + respBody, err := h.AtpClient.GetRepo(ctx, pdsURL, did) 334 + if err != nil { 335 + log.Printf("WARN: [Worker %d] Failed to get repo stream for %s: %v", id, did, err) 336 + return 337 + } 338 + defer respBody.Close() 339 + 340 + if err := os.MkdirAll("./temp", 0o755); err != nil { 341 + panic(err) 342 + } 343 + tempFile, err := ioutil.TempFile("./temp", "backstream-repo-*.car") 344 + if err != nil { 345 + log.Printf("ERROR: [Worker %d] Failed to create temp file for %s: %v", id, did, err) 346 + return 347 + } 348 + defer os.Remove(tempFile.Name()) 349 + 350 + if _, err := io.Copy(tempFile, respBody); err != nil { 351 + log.Printf("ERROR: [Worker %d] Failed to write repo to temp file for %s: %v", id, did, err) 352 + return 353 + } 354 + 355 + if err := tempFile.Close(); err != nil { 356 + log.Printf("ERROR: [Worker %d] Failed to close temp file for %s: %v", id, did, err) 357 + return 358 + } 359 + 360 + readHandle, err := os.Open(tempFile.Name()) 361 + if err != nil { 362 + log.Printf("ERROR: [Worker %d] Failed to open temp file for reading %s: %v", id, did, err) 363 + return 364 + } 365 + defer readHandle.Close() 366 + 367 + _, r, err := atrepo.LoadRepoFromCAR(ctx, readHandle) 368 + if err != nil { 369 + log.Printf("WARN: [Worker %d] Failed to read CAR stream for %s from temp file: %v", id, did, err) 370 + return 371 + } 372 + 373 + err = r.MST.Walk(func(k []byte, v cid.Cid) error { 374 + select { 375 + case <-ctx.Done(): 376 + return errors.New("context cancelled during repo walk") 377 + default: 378 + } 379 + 380 + path := string(k) 381 + collection, rkey, err := syntax.ParseRepoPath(path) 382 + if err != nil { 383 + log.Printf("WARN: [Worker %d] Could not parse repo path '%s' for %s, skipping record", id, path, did) 384 + return nil 385 + } 386 + 387 + if !isAllCollections { 388 + if _, ok := wantedSet[string(collection)]; !ok { 389 + return nil 390 + } 391 + } 392 + 393 + recBytes, _, err := r.GetRecordBytes(ctx, collection, rkey) 394 + if err != nil { 395 + log.Printf("WARN: [Worker %d] Failed to get record bytes for %s: %v", id, path, err) 396 + return nil 397 + } 398 + 399 + recordVal, err := data.UnmarshalCBOR(recBytes) 400 + if err != nil { 401 + log.Printf("WARN: [Worker %d] Failed to unmarshal record CBOR for %s: %v", id, path, err) 402 + return nil 403 + } 404 + 405 + record := Record{ 406 + URI: fmt.Sprintf("at://%s/%s", did, path), 407 + CID: v.String(), 408 + Value: recordVal, 409 + } 410 + 411 + output := h.formatOutput(record, did, string(collection), session.Params.GetRecordFormat) 412 + select { 413 + case results <- output: 414 + case <-ctx.Done(): 415 + return errors.New("context cancelled while sending result") 416 + } 417 + 418 + session.SetListRecordsCursor(did, string(collection), string(rkey)) 419 + return nil 420 + }) 421 + 422 + if err != nil && !errors.Is(err, context.Canceled) { 423 + log.Printf("WARN: [Worker %d] Error while walking repo for %s: %v", id, did, err) 424 + } 425 + } 426 + 427 + func (h *BackfillHandler) processDIDWithListRecords(ctx context.Context, id int, did, pdsURL string, results chan<- interface{}, session *Session) { 428 + log.Printf("[Worker %d] Using listRecords method for %s", id, did) 429 + isAllCollections := len(session.Params.WantedCollections) == 1 && session.Params.WantedCollections[0] == "*" 430 + var collectionsToProcess []string 431 + 432 + if isAllCollections { 433 + repoCollections, err := h.AtpClient.DescribeRepo(ctx, pdsURL, did) 434 + if err != nil { 435 + log.Printf("WARN: [Worker %d] Could not describe repo for %s to find collections, skipping. Error: %v", id, did, err) 436 + return 437 + } 438 + collectionsToProcess = repoCollections 439 + log.Printf("[Worker %d] Found %d collections for DID %s", id, len(collectionsToProcess), did) 440 + } else { 441 + collectionsToProcess = session.Params.WantedCollections 442 + } 443 + 444 + for _, collection := range collectionsToProcess { 445 + cursor := session.GetListRecordsCursor(did, collection) 446 + for { 447 + select { 448 + case <-ctx.Done(): 449 + log.Printf("[Worker %d] Context cancelled for DID %s", id, did) 450 + return 451 + default: 452 + } 453 + 454 + records, nextCursor, err := h.AtpClient.ListRecords(ctx, pdsURL, did, collection, cursor) 455 + if err != nil { 456 + if !strings.Contains(err.Error(), "status: 400") { 457 + log.Printf("WARN: [Worker %d] Failed to list records for %s/%s, skipping. Error: %v", id, did, collection, err) 458 + } 459 + break 460 + } 461 + 462 + for _, record := range records { 463 + output := h.formatOutput(record, did, collection, session.Params.GetRecordFormat) 464 + select { 465 + case results <- output: 466 + case <-ctx.Done(): 467 + log.Printf("[Worker %d] Context cancelled while sending results for %s", id, did) 468 + return 469 + } 470 + } 471 + 472 + session.SetListRecordsCursor(did, collection, nextCursor) 473 + cursor = nextCursor 474 + if cursor == "" { 475 + break 476 + } 477 + } 478 + } 479 + } 480 + 481 + func (h *BackfillHandler) parseQueryParams(r *http.Request) (BackfillParams, string, error) { 482 + query := r.URL.Query() 483 + ticket := query.Get("ticket") 484 + 485 + wantedDidsStr := query.Get("wantedDids") 486 + wantedCollectionsStr := query.Get("wantedCollections") 487 + 488 + if wantedCollectionsStr == "" && wantedDidsStr == "" && ticket == "" { 489 + ticket = "jetstreamfalse" 490 + } else if ticket == "" { 491 + ticket = generateTicket() 492 + } 493 + 494 + if wantedDidsStr == "" { 495 + log.Println("Query parameter 'wantedDids' not specified, defaulting to '*' (all repos).") 496 + wantedDidsStr = "*" 497 + } 498 + 499 + if wantedCollectionsStr == "" { 500 + log.Println("Query parameter 'wantedCollections' not specified, defaulting to '*' (all collections).") 501 + wantedCollectionsStr = "*" 502 + } 503 + 504 + params := BackfillParams{ 505 + WantedDIDs: strings.Split(wantedDidsStr, ","), 506 + WantedCollections: strings.Split(wantedCollectionsStr, ","), 507 + GetRecordFormat: query.Get("getRecordFormat") == "true", 508 + } 509 + return params, ticket, nil 510 + } 511 + 512 + func (h *BackfillHandler) formatOutput(record Record, did, collection string, getRecordFormat bool) interface{} { 513 + if getRecordFormat { 514 + return GetRecordOutput{ 515 + URI: record.URI, 516 + CID: record.CID, 517 + Value: record.Value, 518 + } 519 + } 520 + uriParts := strings.Split(record.URI, "/") 521 + rkey := "" 522 + if len(uriParts) == 5 { 523 + rkey = uriParts[4] 524 + } 525 + return JetstreamLikeOutput{ 526 + Did: did, 527 + Kind: "commit", 528 + TimeUS: "1725911162329308", 529 + Commit: JetstreamLikeCommit{ 530 + Rev: rkey, 531 + Operation: "create", 532 + Collection: collection, 533 + RKey: rkey, 534 + Record: record.Value, 535 + CID: record.CID, 536 + }, 537 + } 538 + } 539 + 540 + func (h *BackfillHandler) sendError(conn *websocket.Conn, message string) { 541 + log.Printf("Sending error to client: %s", message) 542 + _ = conn.WriteJSON(map[string]string{"error": message}) 543 + } 544 + 545 + func (h *BackfillHandler) sendMessage(conn *websocket.Conn, v interface{}) error { 546 + return conn.WriteJSON(v) 547 + }
+113
backstream/session.go
··· 1 + package backstream 2 + 3 + import ( 4 + "crypto/rand" 5 + "encoding/hex" 6 + "log" 7 + "sync" 8 + "time" 9 + ) 10 + 11 + type Session struct { 12 + Ticket string 13 + Params BackfillParams 14 + LastAccessed time.Time 15 + 16 + ListReposCursor string // Cursor for listReposByCollection if wantedDids=* 17 + CompletedDIDs map[string]bool // Set of DIDs that have been fully processed. 18 + listRecordsCursors map[string]string // Key: "did/collection", Value: cursor 19 + 20 + mu sync.Mutex 21 + } 22 + 23 + func (s *Session) GetListRecordsCursor(did, collection string) string { 24 + s.mu.Lock() 25 + defer s.mu.Unlock() 26 + key := did + "/" + collection 27 + return s.listRecordsCursors[key] 28 + } 29 + 30 + func (s *Session) SetListRecordsCursor(did, collection, cursor string) { 31 + s.mu.Lock() 32 + defer s.mu.Unlock() 33 + key := did + "/" + collection 34 + s.listRecordsCursors[key] = cursor 35 + s.LastAccessed = time.Now() 36 + } 37 + 38 + func (s *Session) MarkDIDComplete(did string) { 39 + s.mu.Lock() 40 + defer s.mu.Unlock() 41 + s.CompletedDIDs[did] = true 42 + s.LastAccessed = time.Now() 43 + } 44 + 45 + func (s *Session) IsDIDComplete(did string) bool { 46 + s.mu.Lock() 47 + defer s.mu.Unlock() 48 + return s.CompletedDIDs[did] 49 + } 50 + 51 + type SessionManager struct { 52 + sessions map[string]*Session 53 + ttl time.Duration 54 + mu sync.Mutex 55 + } 56 + 57 + func NewSessionManager(ttl time.Duration) *SessionManager { 58 + sm := &SessionManager{ 59 + sessions: make(map[string]*Session), 60 + ttl: ttl, 61 + } 62 + go sm.cleanupLoop() 63 + return sm 64 + } 65 + 66 + func (sm *SessionManager) GetOrCreate(ticket string, params BackfillParams) *Session { 67 + sm.mu.Lock() 68 + defer sm.mu.Unlock() 69 + 70 + if session, exists := sm.sessions[ticket]; exists { 71 + log.Printf("Resuming existing session for ticket: %s", ticket) 72 + session.LastAccessed = time.Now() 73 + if session.CompletedDIDs == nil { 74 + session.CompletedDIDs = make(map[string]bool) 75 + } 76 + return session 77 + } 78 + 79 + log.Printf("Creating new session for ticket: %s", ticket) 80 + newSession := &Session{ 81 + Ticket: ticket, 82 + Params: params, 83 + LastAccessed: time.Now(), 84 + listRecordsCursors: make(map[string]string), 85 + CompletedDIDs: make(map[string]bool), 86 + } 87 + sm.sessions[ticket] = newSession 88 + return newSession 89 + } 90 + 91 + func (sm *SessionManager) cleanupLoop() { 92 + ticker := time.NewTicker(sm.ttl / 2) 93 + defer ticker.Stop() 94 + for range ticker.C { 95 + sm.mu.Lock() 96 + now := time.Now() 97 + for ticket, session := range sm.sessions { 98 + if now.Sub(session.LastAccessed) > sm.ttl { 99 + log.Printf("Session %s expired. Cleaning up.", ticket) 100 + delete(sm.sessions, ticket) 101 + } 102 + } 103 + sm.mu.Unlock() 104 + } 105 + } 106 + 107 + func generateTicket() string { 108 + bytes := make([]byte, 16) 109 + if _, err := rand.Read(bytes); err != nil { 110 + return "fallback-ticket-" + time.Now().String() 111 + } 112 + return hex.EncodeToString(bytes) 113 + }
+862
cmd/appview/main.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "flag" 8 + "fmt" 9 + "io" 10 + "log" 11 + "net/http" 12 + "os" 13 + "strings" 14 + "sync" 15 + "time" 16 + 17 + did "github.com/whyrusleeping/go-did" 18 + "tangled.org/whey.party/red-dwarf-server/auth" 19 + aturilist "tangled.org/whey.party/red-dwarf-server/cmd/aturilist/client" 20 + "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 21 + "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 22 + appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs" 23 + appbskyfeeddefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/feed/defs" 24 + appbskyunspeccedgetpostthreadv2 "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/unspecced/getpostthreadv2" 25 + "tangled.org/whey.party/red-dwarf-server/shims/utils" 26 + "tangled.org/whey.party/red-dwarf-server/sticket" 27 + "tangled.org/whey.party/red-dwarf-server/store" 28 + 29 + // "github.com/bluesky-social/indigo/atproto/atclient" 30 + // comatproto "github.com/bluesky-social/indigo/api/atproto" 31 + appbsky "github.com/bluesky-social/indigo/api/bsky" 32 + "github.com/bluesky-social/indigo/atproto/syntax" 33 + 34 + // "github.com/bluesky-social/indigo/atproto/atclient" 35 + // "github.com/bluesky-social/indigo/atproto/identity" 36 + // "github.com/bluesky-social/indigo/atproto/syntax" 37 + "github.com/bluesky-social/indigo/api/agnostic" 38 + "github.com/gin-contrib/cors" 39 + "github.com/gin-gonic/gin" 40 + // "github.com/bluesky-social/jetstream/pkg/models" 41 + ) 42 + 43 + var ( 44 + JETSTREAM_URL string 45 + SPACEDUST_URL string 46 + SLINGSHOT_URL string 47 + CONSTELLATION_URL string 48 + ATURILIST_URL string 49 + ) 50 + 51 + func initURLs(prod bool) { 52 + if !prod { 53 + JETSTREAM_URL = "wss://jetstream.whey.party/subscribe" 54 + SPACEDUST_URL = "wss://spacedust.whey.party/subscribe" 55 + SLINGSHOT_URL = "https://slingshot.whey.party" 56 + CONSTELLATION_URL = "https://constellation.whey.party" 57 + ATURILIST_URL = "http://localhost:7155" 58 + } else { 59 + JETSTREAM_URL = "ws://localhost:6008/subscribe" 60 + SPACEDUST_URL = "ws://localhost:9998/subscribe" 61 + SLINGSHOT_URL = "http://localhost:7729" 62 + CONSTELLATION_URL = "http://localhost:7728" 63 + ATURILIST_URL = "http://localhost:7155" 64 + } 65 + } 66 + 67 + const ( 68 + BSKYIMAGECDN_URL = "https://cdn.bsky.app" 69 + BSKYVIDEOCDN_URL = "https://video.bsky.app" 70 + serviceWebDID = "did:web:server.reddwarf.app" 71 + serviceWebHost = "https://server.reddwarf.app" 72 + ) 73 + 74 + func main() { 75 + log.Println("red-dwarf-server AppView Service started") 76 + prod := flag.Bool("prod", false, "use production URLs instead of localhost") 77 + flag.Parse() 78 + 79 + initURLs(*prod) 80 + 81 + ctx := context.Background() 82 + mailbox := sticket.New() 83 + sl := slingshot.NewSlingshot(SLINGSHOT_URL) 84 + cs := constellation.NewConstellation(CONSTELLATION_URL) 85 + al := aturilist.NewClient(ATURILIST_URL) 86 + // spacedust is type definitions only 87 + // jetstream types is probably available from jetstream/pkg/models 88 + 89 + //threadGraphCache := make(map[syntax.ATURI]appbskyunspeccedgetpostthreadv2.ThreadGraph) 90 + 91 + router_raw := gin.New() 92 + router_raw.Use(gin.Logger()) 93 + router_raw.Use(gin.Recovery()) 94 + //router_raw.Use(cors.Default()) 95 + router_raw.Use(cors.New(cors.Config{ 96 + AllowAllOrigins: true, 97 + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, 98 + // You must explicitly allow the custom ATProto headers here 99 + AllowHeaders: []string{ 100 + "Origin", 101 + "Content-Length", 102 + "Content-Type", 103 + "Authorization", 104 + "Accept", 105 + "Accept-Language", 106 + "atproto-accept-labelers", // <--- The specific fix for your error 107 + "atproto-proxy", // Good to have for future compatibility 108 + }, 109 + ExposeHeaders: []string{"Content-Length", "Link"}, 110 + AllowCredentials: true, 111 + MaxAge: 12 * time.Hour, 112 + })) 113 + 114 + router_raw.GET("/.well-known/did.json", GetWellKnownDID) 115 + 116 + auther, err := auth.NewAuth( 117 + 100_000, 118 + time.Hour*12, 119 + 5, 120 + serviceWebDID, //+"#bsky_appview", 121 + ) 122 + if err != nil { 123 + log.Fatalf("Failed to create Auth: %v", err) 124 + } 125 + 126 + router := router_raw.Group("/") 127 + router.Use(auther.AuthenticateGinRequestViaJWT) 128 + 129 + router_unsafe := router_raw.Group("/") 130 + router_unsafe.Use(auther.AuthenticateGinRequestViaJWTUnsafe) 131 + 132 + responsewow, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", "did:web:did12.whey.party", "self") 133 + if err != nil { 134 + log.Println(err) 135 + } 136 + 137 + log.Println(responsewow.Uri) 138 + 139 + var didtest *utils.DID 140 + didval, errdid := utils.NewDID("did:web:did12.whey.party") 141 + if errdid != nil { 142 + didtest = nil 143 + } else { 144 + didtest = &didval 145 + } 146 + profiletest, _, _ := appbskyactordefs.ProfileViewBasic(ctx, *didtest, sl, cs, BSKYIMAGECDN_URL, nil) 147 + 148 + log.Println(*profiletest.DisplayName) 149 + log.Println(*profiletest.Avatar) 150 + 151 + router.GET("/ws", func(c *gin.Context) { 152 + mailbox.HandleWS(c.Writer, c.Request) 153 + }) 154 + 155 + kv := store.NewKV() 156 + 157 + // sad attempt to get putpref working. tldr it wont work without a client fork 158 + // https://bsky.app/profile/did:web:did12.whey.party/post/3m75xtomd722n 159 + router.GET("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 160 + c.Status(200) 161 + }) 162 + router.PUT("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 163 + c.Status(200) 164 + }) 165 + router.POST("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 166 + c.Status(200) 167 + 168 + userDID := c.GetString("user_did") 169 + body, err := io.ReadAll(c.Request.Body) 170 + if err != nil { 171 + c.JSON(400, gin.H{"error": "invalid body"}) 172 + return 173 + } 174 + 175 + kv.Set(userDID, body) 176 + 177 + }) 178 + 179 + router.GET("/xrpc/app.bsky.actor.getPreferences", func(c *gin.Context) { 180 + userDID := c.GetString("user_did") 181 + val, ok := kv.Get(userDID) 182 + if !ok { 183 + c.JSON(200, gin.H{"preferences": []any{}}) 184 + return 185 + } 186 + 187 + c.Data(200, "application/json", val) 188 + 189 + }) 190 + 191 + bskyappdid, _ := utils.NewDID("did:plc:z72i7hdynmk6r22z27h6tvur") 192 + 193 + profiletest2, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, bskyappdid, sl, cs, BSKYIMAGECDN_URL, nil) 194 + 195 + data, err := json.MarshalIndent(profiletest2, "", " ") 196 + if err != nil { 197 + panic(err) 198 + } 199 + fmt.Println(string(data)) 200 + 201 + router.GET("/xrpc/app.bsky.actor.getProfiles", 202 + func(c *gin.Context) { 203 + actors := c.QueryArray("actors") 204 + 205 + rawdid := c.GetString("user_did") 206 + var viewer *utils.DID 207 + didval, errdid := utils.NewDID(rawdid) 208 + if errdid != nil { 209 + viewer = nil 210 + } else { 211 + viewer = &didval 212 + } 213 + 214 + profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors)) 215 + 216 + for _, v := range actors { 217 + did, err := utils.NewDID(v) 218 + if err != nil { 219 + continue 220 + } 221 + profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 222 + profiles = append(profiles, profile) 223 + } 224 + 225 + c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{ 226 + Profiles: profiles, 227 + }) 228 + }) 229 + 230 + router.GET("/xrpc/app.bsky.actor.getProfile", 231 + func(c *gin.Context) { 232 + actor := c.Query("actor") 233 + did, err := utils.NewDID(actor) 234 + if err != nil { 235 + c.JSON(http.StatusBadRequest, nil) 236 + return 237 + } 238 + rawdid := c.GetString("user_did") 239 + var viewer *utils.DID 240 + didval, errdid := utils.NewDID(rawdid) 241 + if errdid != nil { 242 + viewer = nil 243 + } else { 244 + viewer = &didval 245 + } 246 + profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 247 + c.JSON(http.StatusOK, profile) 248 + }) 249 + 250 + // really bad actually 251 + router.GET("/xrpc/app.bsky.notification.listNotifications", 252 + func(c *gin.Context) { 253 + emptyarray := []*appbsky.NotificationListNotifications_Notification{} 254 + notifshim := &appbsky.NotificationListNotifications_Output{ 255 + Notifications: emptyarray, 256 + } 257 + c.JSON(http.StatusOK, notifshim) 258 + }) 259 + 260 + router.GET("/xrpc/app.bsky.labeler.getServices", 261 + func(c *gin.Context) { 262 + dids := c.QueryArray("dids") 263 + 264 + labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids)) 265 + //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids)) 266 + 267 + for _, v := range dids { 268 + did, err := utils.NewDID(v) 269 + if err != nil { 270 + continue 271 + } 272 + rawdid := c.GetString("user_did") 273 + var viewer *utils.DID 274 + didval, errdid := utils.NewDID(rawdid) 275 + if errdid != nil { 276 + viewer = nil 277 + } else { 278 + viewer = &didval 279 + } 280 + labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 281 + labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self") 282 + var labelerservice appbsky.LabelerService 283 + if labelerserviceresponse != nil { 284 + if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil { 285 + continue 286 + } 287 + } 288 + 289 + a := "account" 290 + b := "record" 291 + c := "chat" 292 + 293 + placeholderTypes := []*string{&a, &b, &c} 294 + 295 + labeler := &appbsky.LabelerGetServices_Output_Views_Elem{ 296 + LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{ 297 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"` 298 + LexiconTypeID: "app.bsky.labeler.defs#labelerView", 299 + // Cid string `json:"cid" cborgen:"cid"` 300 + Cid: *labelerserviceresponse.Cid, 301 + // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 302 + Creator: labelerprofile, 303 + // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 304 + IndexedAt: labelerservice.CreatedAt, 305 + // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 306 + Labels: nil, // seems to always be empty? 307 + // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 308 + LikeCount: nil, // placeholder sorry 309 + // Uri string `json:"uri" cborgen:"uri"` 310 + Uri: labelerserviceresponse.Uri, 311 + // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 312 + Viewer: nil, 313 + }, 314 + LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{ 315 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"` 316 + LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed", 317 + // Cid string `json:"cid" cborgen:"cid"` 318 + Cid: *labelerserviceresponse.Cid, 319 + // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 320 + Creator: labelerprofile, 321 + // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 322 + IndexedAt: labelerservice.CreatedAt, 323 + // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 324 + Labels: nil, // seems to always be empty? 325 + // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 326 + LikeCount: nil, // placeholder sorry 327 + // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"` 328 + Policies: labelerservice.Policies, 329 + // // reasonTypes: The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. 330 + // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"` 331 + ReasonTypes: nil, //usually not even present 332 + // // subjectCollections: Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. 333 + // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"` 334 + SubjectCollections: nil, //usually not even present 335 + // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on. 336 + // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"` 337 + SubjectTypes: placeholderTypes, 338 + // Uri string `json:"uri" cborgen:"uri"` 339 + Uri: labelerserviceresponse.Uri, 340 + // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 341 + Viewer: nil, 342 + }, 343 + } 344 + labelers = append(labelers, labeler) 345 + } 346 + 347 + c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{ 348 + Views: labelers, 349 + }) 350 + }) 351 + 352 + router.GET("/xrpc/app.bsky.feed.getFeedGenerators", 353 + func(c *gin.Context) { 354 + feeds := c.QueryArray("feeds") 355 + ctx := c.Request.Context() 356 + 357 + type result struct { 358 + view *appbsky.FeedDefs_GeneratorView 359 + } 360 + 361 + results := make([]result, len(feeds)) 362 + 363 + var wg sync.WaitGroup 364 + wg.Add(len(feeds)) 365 + 366 + for i, raw := range feeds { 367 + go func(i int, raw string) { 368 + defer wg.Done() 369 + 370 + aturi, err := syntax.ParseATURI(raw) 371 + if err != nil { 372 + return 373 + } 374 + 375 + did := aturi.Authority().String() 376 + collection := aturi.Collection().String() 377 + rkey := aturi.RecordKey().String() 378 + 379 + repoDID, err := utils.NewDID(did) 380 + if err != nil { 381 + return 382 + } 383 + rawdid := c.GetString("user_did") 384 + var viewer *utils.DID 385 + didval, errdid := utils.NewDID(rawdid) 386 + if errdid != nil { 387 + viewer = nil 388 + } else { 389 + viewer = &didval 390 + } 391 + 392 + // fetch profile and record in parallel too (optional) 393 + // but to keep it simple, do serial inside this goroutine 394 + profile, _, _ := appbskyactordefs.ProfileView(ctx, repoDID, sl, cs, BSKYIMAGECDN_URL, viewer) 395 + 396 + rec, err := agnostic.RepoGetRecord(ctx, sl, "", collection, did, rkey) 397 + if err != nil || rec.Value == nil { 398 + return 399 + } 400 + 401 + var genRec appbsky.FeedGenerator 402 + if err := json.Unmarshal(*rec.Value, &genRec); err != nil { 403 + return 404 + } 405 + 406 + var avatar *string 407 + if genRec.Avatar != nil { 408 + u := utils.MakeImageCDN(repoDID, BSKYIMAGECDN_URL, "avatar", genRec.Avatar.Ref.String()) 409 + avatar = &u 410 + } 411 + 412 + results[i].view = &appbsky.FeedDefs_GeneratorView{ 413 + LexiconTypeID: "app.bsky.feed.defs#generatorView", 414 + AcceptsInteractions: genRec.AcceptsInteractions, 415 + Avatar: avatar, 416 + Cid: *rec.Cid, 417 + ContentMode: genRec.ContentMode, 418 + Creator: profile, 419 + Description: genRec.Description, 420 + DescriptionFacets: genRec.DescriptionFacets, 421 + Did: did, 422 + DisplayName: genRec.DisplayName, 423 + IndexedAt: genRec.CreatedAt, 424 + Uri: rec.Uri, 425 + } 426 + }(i, raw) 427 + } 428 + 429 + wg.Wait() 430 + 431 + // build final slice 432 + out := make([]*appbsky.FeedDefs_GeneratorView, 0, len(results)) 433 + for _, r := range results { 434 + if r.view != nil { 435 + out = append(out, r.view) 436 + } 437 + } 438 + 439 + c.JSON(http.StatusOK, &appbsky.FeedGetFeedGenerators_Output{ 440 + Feeds: out, 441 + }) 442 + }) 443 + 444 + router.GET("/xrpc/app.bsky.feed.getPosts", 445 + func(c *gin.Context) { 446 + rawdid := c.GetString("user_did") 447 + var viewer *utils.DID 448 + didval, errdid := utils.NewDID(rawdid) 449 + if errdid != nil { 450 + viewer = nil 451 + } else { 452 + viewer = &didval 453 + } 454 + postsreq := c.QueryArray("uris") 455 + ctx := c.Request.Context() 456 + 457 + type result struct { 458 + view *appbsky.FeedDefs_PostView 459 + } 460 + 461 + results := make([]result, len(postsreq)) 462 + 463 + var wg sync.WaitGroup 464 + wg.Add(len(postsreq)) 465 + 466 + for i, raw := range postsreq { 467 + go func(i int, raw string) { 468 + defer wg.Done() 469 + 470 + post, _, _ := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 471 + 472 + results[i].view = post 473 + }(i, raw) 474 + } 475 + 476 + wg.Wait() 477 + 478 + // build final slice 479 + out := make([]*appbsky.FeedDefs_PostView, 0, len(results)) 480 + for _, r := range results { 481 + if r.view != nil { 482 + out = append(out, r.view) 483 + } 484 + } 485 + 486 + c.JSON(http.StatusOK, &appbsky.FeedGetPosts_Output{ 487 + Posts: out, 488 + }) 489 + }) 490 + 491 + router_unsafe.GET("/xrpc/app.bsky.feed.getFeed", 492 + func(c *gin.Context) { 493 + ctx := c.Request.Context() 494 + 495 + rawdid := c.GetString("user_did") 496 + log.Println("getFeed router_unsafe user_did: " + rawdid) 497 + var viewer *utils.DID 498 + didval, errdid := utils.NewDID(rawdid) 499 + if errdid != nil { 500 + viewer = nil 501 + } else { 502 + viewer = &didval 503 + } 504 + 505 + feedGenAturiRaw := c.Query("feed") 506 + if feedGenAturiRaw == "" { 507 + c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 508 + return 509 + } 510 + 511 + feedGenAturi, err := syntax.ParseATURI(feedGenAturiRaw) 512 + if err != nil { 513 + return 514 + } 515 + 516 + feedGeneratorRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.generator", feedGenAturi.Authority().String(), feedGenAturi.RecordKey().String()) 517 + if err != nil { 518 + c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve feed generator record: %v", err)}) 519 + return 520 + } 521 + 522 + var feedGeneratorRecord appbsky.FeedGenerator 523 + if err := json.Unmarshal(*feedGeneratorRecordResponse.Value, &feedGeneratorRecord); err != nil { 524 + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse feed generator record JSON"}) 525 + return 526 + } 527 + 528 + feedGenDID := feedGeneratorRecord.Did 529 + 530 + didDoc, err := ResolveDID(feedGenDID) 531 + if err != nil { 532 + c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)}) 533 + return 534 + } 535 + 536 + var targetEndpoint string 537 + for _, svc := range didDoc.Service { 538 + if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") { 539 + targetEndpoint = svc.ServiceEndpoint 540 + break 541 + } 542 + } 543 + if targetEndpoint == "" { 544 + c.JSON(http.StatusBadGateway, gin.H{"error": "Feed Generator service endpoint not found in DID document"}) 545 + return 546 + } 547 + upstreamURL := fmt.Sprintf("%s/xrpc/app.bsky.feed.getFeedSkeleton?%s", 548 + strings.TrimSuffix(targetEndpoint, "/"), 549 + c.Request.URL.RawQuery, 550 + ) 551 + req, err := http.NewRequestWithContext(ctx, "GET", upstreamURL, nil) 552 + if err != nil { 553 + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upstream request"}) 554 + return 555 + } 556 + headersToForward := []string{"Authorization", "Content-Type", "Accept", "User-Agent"} 557 + for _, k := range headersToForward { 558 + if v := c.GetHeader(k); v != "" { 559 + req.Header.Set(k, v) 560 + } 561 + } 562 + client := &http.Client{} 563 + resp, err := client.Do(req) 564 + if err != nil { 565 + c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Upstream request failed: %v", err)}) 566 + return 567 + } 568 + defer resp.Body.Close() 569 + 570 + bodyBytes, err := io.ReadAll(resp.Body) 571 + if err != nil { 572 + c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read upstream body"}) 573 + return 574 + } 575 + if resp.StatusCode != http.StatusOK { 576 + // Forward the upstream error raw 577 + c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), bodyBytes) 578 + return 579 + } 580 + 581 + var feekskeleton appbsky.FeedGetFeedSkeleton_Output 582 + if err := json.Unmarshal(bodyBytes, &feekskeleton); err != nil { 583 + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream JSON"}) 584 + return 585 + } 586 + 587 + skeletonposts := feekskeleton.Feed 588 + 589 + concurrentResults := MapConcurrent( 590 + ctx, 591 + skeletonposts, 592 + 20, 593 + func(ctx context.Context, raw *appbsky.FeedDefs_SkeletonFeedPost, idx int) (*appbsky.FeedDefs_FeedViewPost, error) { 594 + post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 595 + if err != nil { 596 + return nil, err 597 + } 598 + if post == nil { 599 + return nil, fmt.Errorf("post not found") 600 + } 601 + 602 + return &appbsky.FeedDefs_FeedViewPost{ 603 + // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"` 604 + // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 605 + Post: post, 606 + // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"` 607 + // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{ 608 + // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost 609 + // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{ 610 + // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"` 611 + // LexiconTypeID: "app.bsky.feed.defs#reasonRepost", 612 + // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"` 613 + // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 614 + // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 615 + // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"` 616 + // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost, 617 + // }, 618 + // // FeedDefs_ReasonPin *FeedDefs_ReasonPin 619 + // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{ 620 + // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"` 621 + // LexiconTypeID: "app.bsky.feed.defs#reasonPin", 622 + // }, 623 + // }, 624 + // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"` 625 + // // reqId: Unique identifier per request that may be passed back alongside interactions. 626 + // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"` 627 + }, nil 628 + }, 629 + ) 630 + 631 + // build final slice 632 + out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults)) 633 + for _, r := range concurrentResults { 634 + if r.Err == nil && r.Value != nil && r.Value.Post != nil { 635 + out = append(out, r.Value) 636 + } 637 + } 638 + 639 + c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{ 640 + Cursor: feekskeleton.Cursor, 641 + Feed: out, 642 + }) 643 + }) 644 + type GetPostThreadOtherV2_Output_WithOtherReplies struct { 645 + appbsky.UnspeccedGetPostThreadOtherV2_Output 646 + HasOtherReplies bool `json:"hasOtherReplies"` 647 + } 648 + router.GET("/xrpc/app.bsky.unspecced.getPostThreadV2", 649 + func(c *gin.Context) { 650 + //appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2(c, sl, cs, BSKYIMAGECDN_URL) 651 + // V2V2 still doesnt work. should probably make the handler from scratch to fully use the thread grapher. 652 + // also the thread grapher is still sequental. pls fix that 653 + //appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2V2(c, sl, cs, BSKYIMAGECDN_URL) 654 + 655 + var existingGraph *appbskyunspeccedgetpostthreadv2.ThreadGraph 656 + // var kvkey string 657 + // threadAnchorURIraw := c.Query("anchor") 658 + // if threadAnchorURIraw != "" { 659 + // threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 660 + // if err == nil { 661 + // kvkey = "ThreadGraph" + threadAnchorURI.String() 662 + // val, ok := kv.Get(kvkey) 663 + // if ok { 664 + // parsed, err := appbskyunspeccedgetpostthreadv2.ThreadGraphFromBytes(val) 665 + // if err != nil { 666 + // existingGraph = parsed 667 + // } 668 + // } 669 + // } 670 + // } 671 + 672 + returnedGraph := appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2V3(c, sl, cs, BSKYIMAGECDN_URL, existingGraph) 673 + _ = returnedGraph 674 + // bytes, err := returnedGraph.ToBytes() 675 + // if err == nil && kvkey != "" { 676 + // kv.Set(kvkey, bytes, 1*time.Minute) 677 + // } 678 + }) 679 + 680 + router.GET("/xrpc/app.bsky.feed.getAuthorFeed", 681 + func(c *gin.Context) { 682 + 683 + rawdid := c.GetString("user_did") 684 + log.Println("getFeed router_unsafe user_did: " + rawdid) 685 + var viewer *utils.DID 686 + didval, errdid := utils.NewDID(rawdid) 687 + if errdid != nil { 688 + viewer = nil 689 + } else { 690 + viewer = &didval 691 + } 692 + 693 + actorDidParam := c.Query("actor") 694 + if actorDidParam == "" { 695 + c.JSON(http.StatusBadRequest, gin.H{"error": "Missing actor param"}) 696 + return 697 + } 698 + cursorRawParam := c.Query("cursor") 699 + 700 + listResp, err := al.ListRecords(ctx, actorDidParam, "app.bsky.feed.post", cursorRawParam, true) 701 + if err != nil { 702 + log.Fatalf("Failed to list: %v", err) 703 + } 704 + 705 + concurrentResults := MapConcurrent( 706 + ctx, 707 + listResp.Aturis, 708 + 20, 709 + func(ctx context.Context, raw string, idx int) (*appbsky.FeedDefs_FeedViewPost, error) { 710 + post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 711 + if err != nil { 712 + return nil, err 713 + } 714 + if post == nil { 715 + return nil, fmt.Errorf("post not found") 716 + } 717 + 718 + return &appbsky.FeedDefs_FeedViewPost{ 719 + // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"` 720 + // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 721 + Post: post, 722 + // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"` 723 + // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{ 724 + // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost 725 + // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{ 726 + // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"` 727 + // LexiconTypeID: "app.bsky.feed.defs#reasonRepost", 728 + // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"` 729 + // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 730 + // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 731 + // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"` 732 + // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost, 733 + // }, 734 + // // FeedDefs_ReasonPin *FeedDefs_ReasonPin 735 + // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{ 736 + // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"` 737 + // LexiconTypeID: "app.bsky.feed.defs#reasonPin", 738 + // }, 739 + // }, 740 + // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"` 741 + // // reqId: Unique identifier per request that may be passed back alongside interactions. 742 + // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"` 743 + }, nil 744 + }, 745 + ) 746 + 747 + // build final slice 748 + out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults)) 749 + for _, r := range concurrentResults { 750 + if r.Err == nil && r.Value != nil && r.Value.Post != nil { 751 + out = append(out, r.Value) 752 + } 753 + } 754 + 755 + c.JSON(http.StatusOK, &appbsky.FeedGetAuthorFeed_Output{ 756 + Cursor: &listResp.Cursor, 757 + Feed: out, 758 + }) 759 + }) 760 + 761 + // weird stuff 762 + yourJSONBytes, _ := os.ReadFile("./public/getConfig.json") 763 + router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) { 764 + c.DataFromReader(200, -1, "application/json", 765 + bytes.NewReader(yourJSONBytes), nil) 766 + }) 767 + 768 + router.GET("/", func(c *gin.Context) { 769 + log.Println("hello worldio !") 770 + clientUUID := sticket.GetUUIDFromRequest(c.Request) 771 + hasSticket := clientUUID != "" 772 + if hasSticket { 773 + go func(targetUUID string) { 774 + // simulated heavy processing 775 + time.Sleep(2 * time.Second) 776 + 777 + lateData := map[string]any{ 778 + "postId": 101, 779 + "newComments": []string{ 780 + "Wow great tutorial!", 781 + "I am stuck on step 1.", 782 + }, 783 + } 784 + 785 + success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 786 + if success { 787 + log.Println("Successfully sent late data via Sticket") 788 + } else { 789 + log.Println("Failed to send late data (client disconnected?)") 790 + } 791 + }(clientUUID) 792 + } 793 + c.String(http.StatusOK, ` ____ __________ ____ _ _____ ____ ______ 794 + / __ \/ ____/ __ \ / __ \ | / / | / __ \/ ____/ 795 + / /_/ / __/ / / / / / / / / | /| / / /| | / /_/ / /_ 796 + / _, _/ /___/ /_/ / / /_/ /| |/ |/ / ___ |/ _, _/ __/ 797 + /_/ |_/_____/_____/ /_____/ |__/|__/_/ |_/_/ |_/_/ 798 + _____ __________ _ ____________ 799 + / ___// ____/ __ \ | / / ____/ __ \ 800 + \__ \/ __/ / /_/ / | / / __/ / /_/ / 801 + ___/ / /___/ _, _/| |/ / /___/ _, _/ 802 + /____/_____/_/ |_| |___/_____/_/ |_| 803 + 804 + This is an AT Protocol Application View (AppView) for any application that supports app.bsky.* xrpc methods. 805 + 806 + Most API routes are under /xrpc/ 807 + 808 + Code: https://tangled.org/whey.party/red-dwarf-server 809 + Protocol: https://atproto.com 810 + Try it on: https://new.reddwarf.app 811 + `) 812 + }) 813 + router_raw.Run(":7152") 814 + } 815 + 816 + func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 817 + log.Println("hello worldio !") 818 + } 819 + 820 + type DidResponse struct { 821 + Context []string `json:"@context"` 822 + ID string `json:"id"` 823 + Service []did.Service `json:"service"` 824 + } 825 + 826 + /* 827 + { 828 + id: "#bsky_appview", 829 + type: "BskyAppView", 830 + serviceEndpoint: endpoint, 831 + }, 832 + */ 833 + func GetWellKnownDID(c *gin.Context) { 834 + // Use a custom struct to fix missing omitempty on did.Document 835 + serviceEndpoint := serviceWebHost 836 + serviceDID, err := did.ParseDID(serviceWebDID) 837 + if err != nil { 838 + log.Println(fmt.Errorf("error parsing serviceDID: %w", err)) 839 + return 840 + } 841 + serviceID, err := did.ParseDID("#bsky_appview") 842 + if err != nil { 843 + panic(err) 844 + } 845 + didDoc := did.Document{ 846 + Context: []string{did.CtxDIDv1}, 847 + ID: serviceDID, 848 + Service: []did.Service{ 849 + { 850 + ID: serviceID, 851 + Type: "BskyAppView", 852 + ServiceEndpoint: serviceEndpoint, 853 + }, 854 + }, 855 + } 856 + didResponse := DidResponse{ 857 + Context: didDoc.Context, 858 + ID: didDoc.ID.String(), 859 + Service: didDoc.Service, 860 + } 861 + c.JSON(http.StatusOK, didResponse) 862 + }
+127
cmd/appview/utils.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + "net/http/httputil" 9 + "strings" 10 + "sync" 11 + 12 + "github.com/gin-gonic/gin" 13 + ) 14 + 15 + type DIDDocument struct { 16 + ID string `json:"id"` 17 + Service []struct { 18 + ID string `json:"id"` 19 + Type string `json:"type"` 20 + ServiceEndpoint string `json:"serviceEndpoint"` 21 + } `json:"service"` 22 + } 23 + 24 + func ResolveDID(did string) (*DIDDocument, error) { 25 + var url string 26 + 27 + if strings.HasPrefix(did, "did:plc:") { 28 + // Resolve via PLC Directory 29 + url = "https://plc.directory/" + did 30 + } else if strings.HasPrefix(did, "did:web:") { 31 + // Resolve via Web (simplified) 32 + domain := strings.TrimPrefix(did, "did:web:") 33 + url = "https://" + domain + "/.well-known/did.json" 34 + } else { 35 + return nil, fmt.Errorf("unsupported DID format: %s", did) 36 + } 37 + 38 + resp, err := http.Get(url) 39 + if err != nil { 40 + return nil, err 41 + } 42 + defer resp.Body.Close() 43 + 44 + if resp.StatusCode != http.StatusOK { 45 + return nil, fmt.Errorf("resolver returned status: %d", resp.StatusCode) 46 + } 47 + 48 + var doc DIDDocument 49 + if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { 50 + return nil, err 51 + } 52 + 53 + return &doc, nil 54 + } 55 + 56 + type AsyncResult[T any] struct { 57 + Value T 58 + Err error 59 + } 60 + 61 + func MapConcurrent[T any, R any]( 62 + ctx context.Context, 63 + items []T, 64 + concurrencyLimit int, 65 + mapper func(context.Context, T, int) (R, error), 66 + ) []AsyncResult[R] { 67 + if len(items) == 0 { 68 + return nil 69 + } 70 + 71 + results := make([]AsyncResult[R], len(items)) 72 + var wg sync.WaitGroup 73 + 74 + sem := make(chan struct{}, concurrencyLimit) 75 + 76 + for i, item := range items { 77 + wg.Add(1) 78 + go func(idx int, input T) { 79 + defer wg.Done() 80 + 81 + sem <- struct{}{} 82 + defer func() { <-sem }() 83 + 84 + if ctx.Err() != nil { 85 + results[idx] = AsyncResult[R]{Err: ctx.Err()} 86 + return 87 + } 88 + 89 + val, err := mapper(ctx, input, idx) 90 + results[idx] = AsyncResult[R]{Value: val, Err: err} 91 + }(i, item) 92 + } 93 + 94 + wg.Wait() 95 + return results 96 + } 97 + 98 + func RunConcurrent(tasks ...func() error) []error { 99 + var wg sync.WaitGroup 100 + errs := make([]error, len(tasks)) 101 + 102 + wg.Add(len(tasks)) 103 + 104 + for i, task := range tasks { 105 + go func(i int, t func() error) { 106 + defer wg.Done() 107 + if err := t(); err != nil { 108 + errs[i] = err 109 + } 110 + }(i, task) 111 + } 112 + 113 + wg.Wait() 114 + return errs 115 + } 116 + 117 + func DebugMiddleware() gin.HandlerFunc { 118 + return func(c *gin.Context) { 119 + dump, err := httputil.DumpRequest(c.Request, true) 120 + if err == nil { 121 + fmt.Println("=== RAW REQUEST ===") 122 + fmt.Println(string(dump)) 123 + } 124 + 125 + c.Next() 126 + } 127 + }
+230
cmd/aturilist/client/client.go
··· 1 + package aturilist 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "fmt" 8 + "io" 9 + "net/http" 10 + "net/url" 11 + "time" 12 + ) 13 + 14 + // Constants for the XRPC methods 15 + const ( 16 + MethodListRecords = "app.reddwarf.aturilist.listRecords" 17 + MethodCountRecords = "app.reddwarf.aturilist.countRecords" 18 + MethodIndexRecord = "app.reddwarf.aturilist.indexRecord" 19 + MethodValidateRecord = "app.reddwarf.aturilist.validateRecord" 20 + MethodQueryCollectionRkey = "app.reddwarf.aturilist.queryCollectionRkey" 21 + DefaultProductionHost = "https://aturilist.reddwarf.app" 22 + ) 23 + 24 + // Client is the API client for the Red Dwarf AtURI List Service. 25 + type Client struct { 26 + Host string 27 + HTTPClient *http.Client 28 + // AuthToken is the JWT used for the Authorization header 29 + AuthToken string 30 + } 31 + 32 + // NewClient creates a new client. If host is empty, it defaults to production. 33 + func NewClient(host string) *Client { 34 + if host == "" { 35 + host = DefaultProductionHost 36 + } 37 + return &Client{ 38 + Host: host, 39 + HTTPClient: &http.Client{ 40 + Timeout: 10 * time.Second, 41 + }, 42 + } 43 + } 44 + 45 + // --- Response Models --- 46 + 47 + type ListRecordsResponse struct { 48 + Aturis []string `json:"aturis"` 49 + Count int `json:"count"` 50 + Cursor string `json:"cursor,omitempty"` 51 + } 52 + 53 + type CountRecordsResponse struct { 54 + Repo string `json:"repo"` 55 + Collection string `json:"collection"` 56 + Count int `json:"count"` 57 + } 58 + 59 + type QueryCollectionRkeyResponse struct { 60 + Collection string `json:"collection"` 61 + RKey string `json:"rkey"` 62 + DIDs []string `json:"dids"` 63 + Count int `json:"count"` 64 + } 65 + 66 + type ErrorResponse struct { 67 + Error string `json:"error"` 68 + } 69 + 70 + // --- Request Models --- 71 + 72 + type RecordRequest struct { 73 + Repo string `json:"repo"` 74 + Collection string `json:"collection"` 75 + RKey string `json:"rkey"` 76 + } 77 + 78 + // --- Methods --- 79 + 80 + // ListRecords retrieves a list of AT URIs. 81 + // Set reverse=true to get newest records first. 82 + func (c *Client) ListRecords(ctx context.Context, repo, collection, cursor string, reverse bool) (*ListRecordsResponse, error) { 83 + params := url.Values{} 84 + params.Set("repo", repo) 85 + params.Set("collection", collection) 86 + 87 + if cursor != "" { 88 + params.Set("cursor", cursor) 89 + } 90 + 91 + if reverse { 92 + params.Set("reverse", "true") 93 + } 94 + 95 + var resp ListRecordsResponse 96 + if err := c.doRequest(ctx, http.MethodGet, MethodListRecords, params, nil, &resp); err != nil { 97 + return nil, err 98 + } 99 + 100 + return &resp, nil 101 + } 102 + 103 + // CountRecords returns the total number of records indexed for a collection. 104 + func (c *Client) CountRecords(ctx context.Context, repo, collection string) (*CountRecordsResponse, error) { 105 + params := url.Values{} 106 + params.Set("repo", repo) 107 + params.Set("collection", collection) 108 + 109 + var resp CountRecordsResponse 110 + if err := c.doRequest(ctx, http.MethodGet, MethodCountRecords, params, nil, &resp); err != nil { 111 + return nil, err 112 + } 113 + 114 + return &resp, nil 115 + } 116 + 117 + // IndexRecord triggers a manual index of a specific record. 118 + // This endpoint is rate-limited on the server. 119 + func (c *Client) IndexRecord(ctx context.Context, repo, collection, rkey string) error { 120 + reqBody := RecordRequest{ 121 + Repo: repo, 122 + Collection: collection, 123 + RKey: rkey, 124 + } 125 + 126 + // Server returns 200 OK on success, body is empty or status only. 127 + return c.doRequest(ctx, http.MethodPost, MethodIndexRecord, nil, reqBody, nil) 128 + } 129 + 130 + // ValidateRecord checks if a specific record exists in the local DB. 131 + // Returns true if exists, false if 404, error otherwise. 132 + func (c *Client) ValidateRecord(ctx context.Context, repo, collection, rkey string) (bool, error) { 133 + reqBody := RecordRequest{ 134 + Repo: repo, 135 + Collection: collection, 136 + RKey: rkey, 137 + } 138 + 139 + err := c.doRequest(ctx, http.MethodPost, MethodValidateRecord, nil, reqBody, nil) 140 + if err != nil { 141 + // Parse standard error to see if it was a 404 142 + if clientErr, ok := err.(*ClientError); ok && clientErr.StatusCode == 404 { 143 + return false, nil 144 + } 145 + return false, err 146 + } 147 + 148 + return true, nil 149 + } 150 + 151 + // QueryCollectionRkey returns a list of DIDs that have a specific collection and rkey pair. 152 + func (c *Client) QueryCollectionRkey(ctx context.Context, collection, rkey string) (*QueryCollectionRkeyResponse, error) { 153 + params := url.Values{} 154 + params.Set("collection", collection) 155 + params.Set("rkey", rkey) 156 + 157 + var resp QueryCollectionRkeyResponse 158 + if err := c.doRequest(ctx, http.MethodGet, MethodQueryCollectionRkey, params, nil, &resp); err != nil { 159 + return nil, err 160 + } 161 + 162 + return &resp, nil 163 + } 164 + 165 + // --- Internal Helpers --- 166 + 167 + type ClientError struct { 168 + StatusCode int 169 + Message string 170 + } 171 + 172 + func (e *ClientError) Error() string { 173 + return fmt.Sprintf("api error (status %d): %s", e.StatusCode, e.Message) 174 + } 175 + 176 + func (c *Client) doRequest(ctx context.Context, method, xrpcMethod string, params url.Values, body interface{}, dest interface{}) error { 177 + u, err := url.Parse(fmt.Sprintf("%s/xrpc/%s", c.Host, xrpcMethod)) 178 + if err != nil { 179 + return fmt.Errorf("invalid url: %w", err) 180 + } 181 + 182 + if len(params) > 0 { 183 + u.RawQuery = params.Encode() 184 + } 185 + 186 + var bodyReader io.Reader 187 + if body != nil { 188 + jsonBytes, err := json.Marshal(body) 189 + if err != nil { 190 + return fmt.Errorf("failed to marshal body: %w", err) 191 + } 192 + bodyReader = bytes.NewBuffer(jsonBytes) 193 + } 194 + 195 + req, err := http.NewRequestWithContext(ctx, method, u.String(), bodyReader) 196 + if err != nil { 197 + return fmt.Errorf("failed to create request: %w", err) 198 + } 199 + 200 + req.Header.Set("Content-Type", "application/json") 201 + if c.AuthToken != "" { 202 + req.Header.Set("Authorization", "Bearer "+c.AuthToken) 203 + } 204 + 205 + resp, err := c.HTTPClient.Do(req) 206 + if err != nil { 207 + return fmt.Errorf("request failed: %w", err) 208 + } 209 + defer resp.Body.Close() 210 + 211 + // Handle non-200 responses 212 + if resp.StatusCode < 200 || resp.StatusCode >= 300 { 213 + var errResp ErrorResponse 214 + // Try to decode server error message 215 + if decodeErr := json.NewDecoder(resp.Body).Decode(&errResp); decodeErr == nil && errResp.Error != "" { 216 + return &ClientError{StatusCode: resp.StatusCode, Message: errResp.Error} 217 + } 218 + // Fallback if JSON decode fails or empty 219 + return &ClientError{StatusCode: resp.StatusCode, Message: resp.Status} 220 + } 221 + 222 + // Decode response if destination provided 223 + if dest != nil { 224 + if err := json.NewDecoder(resp.Body).Decode(dest); err != nil { 225 + return fmt.Errorf("failed to decode response: %w", err) 226 + } 227 + } 228 + 229 + return nil 230 + }
+498
cmd/aturilist/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "flag" 8 + "fmt" 9 + "log" 10 + "log/slog" 11 + "os" 12 + "strings" 13 + "sync" 14 + "time" 15 + 16 + "github.com/bluesky-social/indigo/api/agnostic" 17 + "github.com/bluesky-social/indigo/atproto/syntax" 18 + "github.com/bluesky-social/jetstream/pkg/client" 19 + "github.com/bluesky-social/jetstream/pkg/client/schedulers/sequential" 20 + "github.com/bluesky-social/jetstream/pkg/models" 21 + "github.com/dgraph-io/badger/v4" 22 + "github.com/gin-gonic/gin" 23 + 24 + "tangled.org/whey.party/red-dwarf-server/auth" 25 + "tangled.org/whey.party/red-dwarf-server/microcosm" 26 + "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 27 + ) 28 + 29 + type Server struct { 30 + db *badger.DB 31 + logger *slog.Logger 32 + 33 + backfillTracker map[string]*sync.WaitGroup 34 + backfillMutex sync.Mutex 35 + } 36 + 37 + var ( 38 + JETSTREAM_URL string 39 + SPACEDUST_URL string 40 + SLINGSHOT_URL string 41 + CONSTELLATION_URL string 42 + ) 43 + 44 + func initURLs(prod bool) { 45 + if !prod { 46 + JETSTREAM_URL = "wss://jetstream.whey.party/subscribe" 47 + SPACEDUST_URL = "wss://spacedust.whey.party/subscribe" 48 + SLINGSHOT_URL = "https://slingshot.whey.party" 49 + CONSTELLATION_URL = "https://constellation.whey.party" 50 + } else { 51 + JETSTREAM_URL = "ws://localhost:6008/subscribe" 52 + SPACEDUST_URL = "ws://localhost:9998/subscribe" 53 + SLINGSHOT_URL = "http://localhost:7729" 54 + CONSTELLATION_URL = "http://localhost:7728" 55 + } 56 + } 57 + 58 + const ( 59 + BSKYIMAGECDN_URL = "https://cdn.bsky.app" 60 + BSKYVIDEOCDN_URL = "https://video.bsky.app" 61 + serviceWebDID = "did:web:aturilist.reddwarf.app" 62 + serviceWebHost = "https://aturilist.reddwarf.app" 63 + ) 64 + 65 + func main() { 66 + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 67 + log.Println("red-dwarf-server AtURI List Service started") 68 + 69 + prod := flag.Bool("prod", false, "use production URLs instead of localhost") 70 + dbPath := flag.String("db", "./badger_data", "path to badger db") 71 + flag.Parse() 72 + 73 + initURLs(*prod) 74 + 75 + db, err := badger.Open(badger.DefaultOptions(*dbPath)) 76 + if err != nil { 77 + logger.Error("Failed to open BadgerDB", "error", err) 78 + os.Exit(1) 79 + } 80 + defer db.Close() 81 + 82 + srv := &Server{ 83 + db: db, 84 + logger: logger, 85 + } 86 + 87 + auther, err := auth.NewAuth( 88 + 100_000, 89 + time.Hour*12, 90 + 5, 91 + serviceWebDID, 92 + ) 93 + if err != nil { 94 + log.Fatalf("Failed to create Auth: %v", err) 95 + } 96 + 97 + ctx := context.Background() 98 + sl := slingshot.NewSlingshot(SLINGSHOT_URL) 99 + 100 + config := client.DefaultClientConfig() 101 + config.WebsocketURL = JETSTREAM_URL 102 + config.Compress = true 103 + 104 + handler := &JetstreamHandler{srv: srv} 105 + scheduler := sequential.NewScheduler("my_app", logger, handler.HandleEvent) 106 + 107 + c, err := client.NewClient(config, logger, scheduler) 108 + if err != nil { 109 + logger.Error("failed to create client", "error", err) 110 + return 111 + } 112 + 113 + cursor := time.Now().Add(-5 * time.Minute).UnixMicro() 114 + 115 + go func() { 116 + logger.Info("Connecting to Jetstream...") 117 + for { 118 + if err := c.ConnectAndRead(ctx, &cursor); err != nil { 119 + logger.Error("jetstream connection disconnected", "error", err) 120 + } 121 + 122 + select { 123 + case <-ctx.Done(): 124 + return 125 + default: 126 + logger.Info("Reconnecting to Jetstream in 5 seconds...", "cursor", cursor) 127 + time.Sleep(5 * time.Second) 128 + } 129 + } 130 + }() 131 + 132 + router := gin.New() 133 + router.Use(auther.AuthenticateGinRequestViaJWT) 134 + 135 + router.GET("/xrpc/app.reddwarf.aturilist.listRecords", srv.handleListRecords) 136 + 137 + router.GET("/xrpc/app.reddwarf.aturilist.countRecords", srv.handleCountRecords) 138 + 139 + router.POST("/xrpc/app.reddwarf.aturilist.indexRecord", func(c *gin.Context) { 140 + srv.handleIndexRecord(c, sl) 141 + }) 142 + 143 + router.POST("/xrpc/app.reddwarf.aturilist.validateRecord", srv.handleValidateRecord) 144 + 145 + router.GET("/xrpc/app.reddwarf.aturilist.queryCollectionRkey", srv.handleQueryCollectionRkey) 146 + 147 + // router.GET("/xrpc/app.reddwarf.aturilist.requestBackfill", ) 148 + 149 + router.Run(":7155") 150 + } 151 + 152 + type JetstreamHandler struct { 153 + srv *Server 154 + } 155 + 156 + func (h *JetstreamHandler) HandleEvent(ctx context.Context, event *models.Event) error { 157 + if event != nil { 158 + if event.Commit != nil { 159 + isDelete := event.Commit.Operation == models.CommitOperationDelete 160 + 161 + h.srv.processRecord(event.Did, event.Commit.Collection, event.Commit.RKey, isDelete) 162 + 163 + } 164 + } 165 + return nil 166 + } 167 + 168 + func makeKey(repo, collection, rkey string) []byte { 169 + return []byte(fmt.Sprintf("%s|%s|%s", repo, collection, rkey)) 170 + } 171 + 172 + func parseKey(key []byte) (repo, collection, rkey string, err error) { 173 + parts := strings.Split(string(key), "|") 174 + if len(parts) != 3 { 175 + return "", "", "", errors.New("invalid key format") 176 + } 177 + return parts[0], parts[1], parts[2], nil 178 + } 179 + 180 + func makeCollectionRkeyKey(collection, rkey string) []byte { 181 + return []byte(fmt.Sprintf("cr|%s|%s|", collection, rkey)) 182 + } 183 + 184 + func parseCollectionRkeyKey(key []byte) (collection, rkey string, err error) { 185 + parts := strings.Split(string(key), "|") 186 + if len(parts) < 3 || parts[0] != "cr" { 187 + return "", "", errors.New("invalid collection+rkey key format") 188 + } 189 + return parts[1], parts[2], nil 190 + } 191 + 192 + func (s *Server) processRecord(repo, collection, rkey string, isDelete bool) { 193 + key := makeKey(repo, collection, rkey) 194 + crKey := makeCollectionRkeyKey(collection, rkey) 195 + 196 + err := s.db.Update(func(txn *badger.Txn) error { 197 + if isDelete { 198 + if err := txn.Delete(key); err != nil { 199 + return err 200 + } 201 + return s.removeDidFromCollectionRkeyIndex(txn, crKey, repo) 202 + } 203 + if err := txn.Set(key, []byte(time.Now().Format(time.RFC3339))); err != nil { 204 + return err 205 + } 206 + return s.addDidToCollectionRkeyIndex(txn, crKey, repo) 207 + }) 208 + 209 + if err != nil { 210 + s.logger.Error("Failed to update DB", "repo", repo, "rkey", rkey, "err", err) 211 + } 212 + } 213 + 214 + func (s *Server) addDidToCollectionRkeyIndex(txn *badger.Txn, crKey []byte, did string) error { 215 + item, err := txn.Get(crKey) 216 + if err == badger.ErrKeyNotFound { 217 + var dids []string 218 + dids = append(dids, did) 219 + didsJSON, _ := json.Marshal(dids) 220 + return txn.Set(crKey, didsJSON) 221 + } else if err != nil { 222 + return err 223 + } 224 + 225 + var dids []string 226 + err = item.Value(func(val []byte) error { 227 + return json.Unmarshal(val, &dids) 228 + }) 229 + if err != nil { 230 + return err 231 + } 232 + 233 + for _, existingDid := range dids { 234 + if existingDid == did { 235 + return nil 236 + } 237 + } 238 + 239 + dids = append(dids, did) 240 + didsJSON, _ := json.Marshal(dids) 241 + return txn.Set(crKey, didsJSON) 242 + } 243 + 244 + func (s *Server) removeDidFromCollectionRkeyIndex(txn *badger.Txn, crKey []byte, did string) error { 245 + item, err := txn.Get(crKey) 246 + if err == badger.ErrKeyNotFound { 247 + return nil 248 + } else if err != nil { 249 + return err 250 + } 251 + 252 + var dids []string 253 + err = item.Value(func(val []byte) error { 254 + return json.Unmarshal(val, &dids) 255 + }) 256 + if err != nil { 257 + return err 258 + } 259 + 260 + var newDids []string 261 + for _, existingDid := range dids { 262 + if existingDid != did { 263 + newDids = append(newDids, existingDid) 264 + } 265 + } 266 + 267 + if len(newDids) == 0 { 268 + return txn.Delete(crKey) 269 + } 270 + 271 + didsJSON, _ := json.Marshal(newDids) 272 + return txn.Set(crKey, didsJSON) 273 + } 274 + 275 + func (s *Server) handleListRecords(c *gin.Context) { 276 + repo := c.Query("repo") 277 + collection := c.Query("collection") 278 + cursor := c.Query("cursor") 279 + reverse := c.Query("reverse") == "true" 280 + limit := 50 281 + 282 + if repo == "" || collection == "" { 283 + c.JSON(400, gin.H{"error": "repo and collection required"}) 284 + return 285 + } 286 + 287 + prefixStr := fmt.Sprintf("%s|%s|", repo, collection) 288 + prefix := []byte(prefixStr) 289 + 290 + var aturis []string 291 + var lastRkey string 292 + 293 + err := s.db.View(func(txn *badger.Txn) error { 294 + opts := badger.DefaultIteratorOptions 295 + opts.PrefetchValues = false 296 + opts.Reverse = reverse 297 + 298 + it := txn.NewIterator(opts) 299 + defer it.Close() 300 + 301 + var startKey []byte 302 + if cursor != "" { 303 + startKey = makeKey(repo, collection, cursor) 304 + } else { 305 + if reverse { 306 + startKey = append([]byte(prefixStr), 0xFF) 307 + } else { 308 + startKey = prefix 309 + } 310 + } 311 + 312 + it.Seek(startKey) 313 + 314 + if cursor != "" && it.Valid() { 315 + if string(it.Item().Key()) == string(startKey) { 316 + it.Next() 317 + } 318 + } 319 + 320 + for ; it.ValidForPrefix(prefix); it.Next() { 321 + if len(aturis) >= limit { 322 + break 323 + } 324 + item := it.Item() 325 + k := item.Key() 326 + _, _, rkey, err := parseKey(k) 327 + if err == nil { 328 + aturis = append(aturis, fmt.Sprintf("at://%s/%s/%s", repo, collection, rkey)) 329 + lastRkey = rkey 330 + } 331 + } 332 + return nil 333 + }) 334 + 335 + if err != nil { 336 + c.JSON(500, gin.H{"error": err.Error()}) 337 + return 338 + } 339 + 340 + resp := gin.H{ 341 + "aturis": aturis, 342 + "count": len(aturis), 343 + } 344 + 345 + if lastRkey != "" && len(aturis) == limit { 346 + resp["cursor"] = lastRkey 347 + } 348 + 349 + c.JSON(200, resp) 350 + } 351 + 352 + func (s *Server) handleCountRecords(c *gin.Context) { 353 + repo := c.Query("repo") 354 + collection := c.Query("collection") 355 + 356 + if repo == "" || collection == "" { 357 + c.JSON(400, gin.H{"error": "repo and collection required"}) 358 + return 359 + } 360 + 361 + prefix := []byte(fmt.Sprintf("%s|%s|", repo, collection)) 362 + count := 0 363 + 364 + err := s.db.View(func(txn *badger.Txn) error { 365 + opts := badger.DefaultIteratorOptions 366 + opts.PrefetchValues = false 367 + it := txn.NewIterator(opts) 368 + defer it.Close() 369 + 370 + for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { 371 + count++ 372 + } 373 + return nil 374 + }) 375 + 376 + if err != nil { 377 + c.JSON(500, gin.H{"error": err.Error()}) 378 + return 379 + } 380 + 381 + c.JSON(200, gin.H{ 382 + "repo": repo, 383 + "collection": collection, 384 + "count": count, 385 + }) 386 + } 387 + 388 + func (s *Server) handleIndexRecord(c *gin.Context, sl *microcosm.MicrocosmClient) { 389 + var req struct { 390 + Collection string `json:"collection"` 391 + Repo string `json:"repo"` 392 + RKey string `json:"rkey"` 393 + } 394 + 395 + if err := c.BindJSON(&req); err != nil { 396 + req.Collection = c.PostForm("collection") 397 + req.Repo = c.PostForm("repo") 398 + req.RKey = c.PostForm("rkey") 399 + } 400 + 401 + if req.Collection == "" || req.Repo == "" || req.RKey == "" { 402 + c.JSON(400, gin.H{"error": "invalid parameters"}) 403 + return 404 + } 405 + 406 + recordResponse, err := agnostic.RepoGetRecord(c.Request.Context(), sl, "", req.Collection, req.Repo, req.RKey) 407 + if err != nil { 408 + s.processRecord(req.Repo, req.Collection, req.RKey, true) 409 + 410 + c.Status(200) 411 + return 412 + } 413 + 414 + uri := recordResponse.Uri 415 + aturi, err := syntax.ParseATURI(uri) 416 + if err != nil { 417 + c.JSON(400, gin.H{"error": "failed to parse aturi from remote"}) 418 + return 419 + } 420 + 421 + s.processRecord(aturi.Authority().String(), string(aturi.Collection()), string(aturi.RecordKey()), false) 422 + c.Status(200) 423 + } 424 + 425 + func (s *Server) handleValidateRecord(c *gin.Context) { 426 + var req struct { 427 + Collection string `json:"collection"` 428 + Repo string `json:"repo"` 429 + RKey string `json:"rkey"` 430 + } 431 + if err := c.BindJSON(&req); err != nil { 432 + c.JSON(400, gin.H{"error": "invalid json"}) 433 + return 434 + } 435 + 436 + key := makeKey(req.Repo, req.Collection, req.RKey) 437 + exists := false 438 + 439 + err := s.db.View(func(txn *badger.Txn) error { 440 + _, err := txn.Get(key) 441 + if err == nil { 442 + exists = true 443 + } else if err == badger.ErrKeyNotFound { 444 + exists = false 445 + return nil 446 + } 447 + return err 448 + }) 449 + 450 + if err != nil { 451 + c.JSON(500, gin.H{"error": err.Error()}) 452 + return 453 + } 454 + 455 + if exists { 456 + c.Status(200) 457 + } else { 458 + c.Status(404) 459 + } 460 + } 461 + 462 + func (s *Server) handleQueryCollectionRkey(c *gin.Context) { 463 + collection := c.Query("collection") 464 + rkey := c.Query("rkey") 465 + 466 + if collection == "" || rkey == "" { 467 + c.JSON(400, gin.H{"error": "collection and rkey required"}) 468 + return 469 + } 470 + 471 + crKey := makeCollectionRkeyKey(collection, rkey) 472 + var dids []string 473 + 474 + err := s.db.View(func(txn *badger.Txn) error { 475 + item, err := txn.Get(crKey) 476 + if err == badger.ErrKeyNotFound { 477 + return nil 478 + } else if err != nil { 479 + return err 480 + } 481 + 482 + return item.Value(func(val []byte) error { 483 + return json.Unmarshal(val, &dids) 484 + }) 485 + }) 486 + 487 + if err != nil { 488 + c.JSON(500, gin.H{"error": err.Error()}) 489 + return 490 + } 491 + 492 + c.JSON(200, gin.H{ 493 + "collection": collection, 494 + "rkey": rkey, 495 + "dids": dids, 496 + "count": len(dids), 497 + }) 498 + }
cmd/backstream/jetstream-zstd-dict.bin

This is a binary file and will not be displayed.

+77
cmd/backstream/main.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net/http" 7 + "os" 8 + "runtime/debug" 9 + "time" 10 + 11 + "tangled.org/whey.party/red-dwarf-server/backstream" 12 + 13 + _ "net/http/pprof" 14 + ) 15 + 16 + const ( 17 + defaultRelay = "https://relay1.us-west.bsky.network" 18 + 19 + plcDirectory = "https://plc.directory" 20 + 21 + zstdDictionaryPath = "jetstream-zstd-dict.bin" 22 + 23 + useGetRepoMethod = true 24 + ) 25 + 26 + func rootHandler(w http.ResponseWriter, r *http.Request) { 27 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 28 + fmt.Fprint(w, `Welcome to Backstream`) 29 + } 30 + 31 + func main() { 32 + debug.SetMemoryLimit(2 << 30) // 2 GB 33 + go func() { 34 + log.Println(http.ListenAndServe("localhost:6060", nil)) 35 + }() 36 + log.Println("Starting Bluesky Backfill Service...") 37 + if useGetRepoMethod { 38 + log.Println("INFO: Using efficient 'getRepo' CAR file method for backfills.") 39 + } else { 40 + log.Println("INFO: Using 'listRecords' pagination method for backfills.") 41 + } 42 + 43 + zstdDict, err := os.ReadFile(zstdDictionaryPath) 44 + if err != nil { 45 + log.Printf("WARN: Could not read zstd dictionary file '%s': %v", zstdDictionaryPath, err) 46 + log.Println("WARN: Zstd compression will be unavailable.") 47 + } else { 48 + log.Printf("Successfully loaded zstd dictionary (%d bytes).", len(zstdDict)) 49 + } 50 + 51 + sessionManager := backstream.NewSessionManager(5 * time.Minute) 52 + 53 + atpClient := backstream.NewATProtoClient(defaultRelay, plcDirectory) 54 + 55 + backfillHandler := &backstream.BackfillHandler{ 56 + Upgrader: backstream.DefaultUpgrader, 57 + SessionManager: sessionManager, 58 + AtpClient: atpClient, 59 + ZstdDict: zstdDict, 60 + UseGetRepoMethod: useGetRepoMethod, 61 + } 62 + 63 + http.Handle("/subscribe", backfillHandler) 64 + 65 + http.HandleFunc("/", rootHandler) 66 + 67 + log.Println("Server listening on :3877") 68 + log.Println("Connect via WebSocket to: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=*") 69 + log.Println("---") 70 + log.Println("Example with specific DIDs: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=did:plc:abc,did:plc:xyz") 71 + log.Println("Example with ticket for resumable session: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=*&ticket=my-session-123") 72 + log.Println("Example with alternative output format: ws://localhost:3877/subscribe?wantedCollections=app.bsky.feed.post&wantedDids=*&getRecordFormat=true") 73 + 74 + if err := http.ListenAndServe(":3877", nil); err != nil { 75 + log.Fatalf("Failed to start server: %v", err) 76 + } 77 + }
+309
cmd/jetrelay/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "log" 9 + "net/http" 10 + "sort" 11 + "sync" 12 + "time" 13 + 14 + "github.com/gorilla/websocket" 15 + "github.com/klauspost/compress/zstd" 16 + ) 17 + 18 + const ( 19 + ServerPort = ":3878" 20 + DictionaryURL = "https://raw.githubusercontent.com/bluesky-social/jetstream/main/pkg/models/zstd_dictionary" 21 + BufferSize = 100000 22 + ReconnectDelay = 5 * time.Second 23 + ) 24 + 25 + var SourceJetstreams = []string{ 26 + "ws://localhost:6008/subscribe", // local jetstream 27 + "ws://localhost:3877/subscribe", // local backstream 28 + } 29 + 30 + type Event struct { 31 + Kind string `json:"kind"` 32 + TimeUS int64 `json:"time_us"` 33 + Commit json.RawMessage `json:"commit,omitempty"` 34 + } 35 + 36 + type BufferedEvent struct { 37 + RelayTimeUS int64 38 + RawJSON []byte 39 + } 40 + 41 + type History struct { 42 + events []BufferedEvent 43 + mu sync.RWMutex 44 + } 45 + 46 + func (h *History) Add(jsonBytes []byte, relayTime int64) { 47 + h.mu.Lock() 48 + defer h.mu.Unlock() 49 + 50 + h.events = append(h.events, BufferedEvent{ 51 + RelayTimeUS: relayTime, 52 + RawJSON: jsonBytes, 53 + }) 54 + 55 + if len(h.events) > BufferSize { 56 + h.events = h.events[len(h.events)-BufferSize:] 57 + } 58 + } 59 + 60 + func (h *History) GetSince(cursor int64) []BufferedEvent { 61 + h.mu.RLock() 62 + defer h.mu.RUnlock() 63 + 64 + idx := sort.Search(len(h.events), func(i int) bool { 65 + return h.events[i].RelayTimeUS > cursor 66 + }) 67 + 68 + if idx < len(h.events) { 69 + result := make([]BufferedEvent, len(h.events)-idx) 70 + copy(result, h.events[idx:]) 71 + return result 72 + } 73 + return nil 74 + } 75 + 76 + var ( 77 + history = &History{events: make([]BufferedEvent, 0, BufferSize)} 78 + zstdDict []byte 79 + hub *Hub 80 + upgrader = websocket.Upgrader{ 81 + CheckOrigin: func(r *http.Request) bool { return true }, 82 + } 83 + ) 84 + 85 + func main() { 86 + log.Println("Initializing Relay...") 87 + 88 + var err error 89 + zstdDict, err = downloadDictionary() 90 + if err != nil { 91 + log.Fatalf("Failed to load dictionary: %v", err) 92 + } 93 + 94 + hub = newHub() 95 + go hub.run() 96 + 97 + ctx := context.Background() 98 + for i, url := range SourceJetstreams { 99 + go runUpstreamConsumer(ctx, i, url) 100 + } 101 + 102 + http.HandleFunc("/subscribe", serveWs) 103 + log.Printf("๐Ÿ”ฅ Relay Active on %s", ServerPort) 104 + if err := http.ListenAndServe(ServerPort, nil); err != nil { 105 + log.Fatal(err) 106 + } 107 + } 108 + 109 + func runUpstreamConsumer(ctx context.Context, id int, baseURL string) { 110 + var lastSeenCursor int64 = 0 111 + 112 + for { 113 + connectURL := baseURL 114 + if lastSeenCursor > 0 { 115 + connectURL = fmt.Sprintf("%s?cursor=%d", baseURL, lastSeenCursor) 116 + log.Printf("[Input %d] Reconnecting with cursor: %d", id, lastSeenCursor) 117 + } else { 118 + log.Printf("[Input %d] Connecting fresh...", id) 119 + } 120 + 121 + conn, _, err := websocket.DefaultDialer.Dial(connectURL, nil) 122 + if err != nil { 123 + log.Printf("[Input %d] Connect failed: %v. Retrying...", id, err) 124 + time.Sleep(ReconnectDelay) 125 + continue 126 + } 127 + 128 + log.Printf("[Input %d] Connected.", id) 129 + 130 + for { 131 + _, msg, err := conn.ReadMessage() 132 + if err != nil { 133 + log.Printf("[Input %d] Read error: %v", id, err) 134 + break 135 + } 136 + 137 + var genericEvent map[string]interface{} 138 + if err := json.Unmarshal(msg, &genericEvent); err != nil { 139 + continue 140 + } 141 + 142 + if t, ok := genericEvent["time_us"].(float64); ok { 143 + lastSeenCursor = int64(t) 144 + } 145 + 146 + nowUS := time.Now().UnixMicro() 147 + genericEvent["time_us"] = nowUS 148 + 149 + finalBytes, err := json.Marshal(genericEvent) 150 + if err != nil { 151 + continue 152 + } 153 + 154 + history.Add(finalBytes, nowUS) 155 + 156 + hub.broadcast <- BufferedEvent{RelayTimeUS: nowUS, RawJSON: finalBytes} 157 + } 158 + conn.Close() 159 + time.Sleep(ReconnectDelay) 160 + } 161 + } 162 + 163 + func serveWs(w http.ResponseWriter, r *http.Request) { 164 + conn, err := upgrader.Upgrade(w, r, nil) 165 + if err != nil { 166 + return 167 + } 168 + 169 + compress := r.URL.Query().Get("compress") == "true" 170 + 171 + var clientCursor int64 = 0 172 + cursorStr := r.URL.Query().Get("cursor") 173 + if cursorStr != "" { 174 + fmt.Sscanf(cursorStr, "%d", &clientCursor) 175 + } 176 + 177 + client := &Client{ 178 + hub: hub, 179 + conn: conn, 180 + send: make(chan BufferedEvent, 2048), 181 + compress: compress, 182 + lastSentUS: 0, 183 + } 184 + 185 + if compress { 186 + enc, _ := zstd.NewWriter(nil, zstd.WithEncoderDict(zstdDict)) 187 + client.encoder = enc 188 + } 189 + 190 + client.hub.register <- client 191 + 192 + go client.writePump() 193 + 194 + if clientCursor > 0 { 195 + log.Printf("Client requested replay from %d", clientCursor) 196 + missedEvents := history.GetSince(clientCursor) 197 + for _, evt := range missedEvents { 198 + client.send <- evt 199 + } 200 + } 201 + 202 + go client.readPump() 203 + } 204 + 205 + type Client struct { 206 + hub *Hub 207 + conn *websocket.Conn 208 + send chan BufferedEvent 209 + compress bool 210 + encoder *zstd.Encoder 211 + lastSentUS int64 212 + } 213 + 214 + type Hub struct { 215 + clients map[*Client]bool 216 + broadcast chan BufferedEvent 217 + register chan *Client 218 + unregister chan *Client 219 + mu sync.RWMutex 220 + } 221 + 222 + func newHub() *Hub { 223 + return &Hub{ 224 + clients: make(map[*Client]bool), 225 + broadcast: make(chan BufferedEvent, 10000), 226 + register: make(chan *Client), 227 + unregister: make(chan *Client), 228 + } 229 + } 230 + 231 + func (h *Hub) run() { 232 + for { 233 + select { 234 + case client := <-h.register: 235 + h.mu.Lock() 236 + h.clients[client] = true 237 + h.mu.Unlock() 238 + 239 + case client := <-h.unregister: 240 + h.mu.Lock() 241 + if _, ok := h.clients[client]; ok { 242 + delete(h.clients, client) 243 + close(client.send) 244 + if client.encoder != nil { 245 + client.encoder.Close() 246 + } 247 + } 248 + h.mu.Unlock() 249 + 250 + case msg := <-h.broadcast: 251 + h.mu.RLock() 252 + for client := range h.clients { 253 + select { 254 + case client.send <- msg: 255 + default: 256 + go func(c *Client) { 257 + h.unregister <- c 258 + c.conn.Close() 259 + }(client) 260 + } 261 + } 262 + h.mu.RUnlock() 263 + } 264 + } 265 + } 266 + 267 + func (c *Client) writePump() { 268 + defer c.conn.Close() 269 + 270 + for msg := range c.send { 271 + if msg.RelayTimeUS <= c.lastSentUS { 272 + continue 273 + } 274 + 275 + c.lastSentUS = msg.RelayTimeUS 276 + 277 + if c.compress { 278 + compressed := c.encoder.EncodeAll(msg.RawJSON, nil) 279 + if err := c.conn.WriteMessage(websocket.BinaryMessage, compressed); err != nil { 280 + return 281 + } 282 + } else { 283 + if err := c.conn.WriteMessage(websocket.TextMessage, msg.RawJSON); err != nil { 284 + return 285 + } 286 + } 287 + } 288 + } 289 + 290 + func (c *Client) readPump() { 291 + defer func() { 292 + c.hub.unregister <- c 293 + c.conn.Close() 294 + }() 295 + for { 296 + if _, _, err := c.conn.ReadMessage(); err != nil { 297 + break 298 + } 299 + } 300 + } 301 + 302 + func downloadDictionary() ([]byte, error) { 303 + resp, err := http.Get(DictionaryURL) 304 + if err != nil { 305 + return nil, err 306 + } 307 + defer resp.Body.Close() 308 + return io.ReadAll(resp.Body) 309 + }
+53 -7
go.mod
··· 3 3 go 1.25.4 4 4 5 5 require ( 6 - github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc 6 + github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635 7 7 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 8 + github.com/gin-contrib/cors v1.7.6 8 9 github.com/gin-gonic/gin v1.11.0 9 10 github.com/golang-jwt/jwt v3.2.2+incompatible 10 11 github.com/google/uuid v1.6.0 11 12 github.com/gorilla/websocket v1.5.3 12 13 github.com/hashicorp/golang-lru/arc/v2 v2.0.7 14 + github.com/klauspost/compress v1.18.2 13 15 github.com/prometheus/client_golang v1.23.2 14 16 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b 15 17 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 ··· 18 20 ) 19 21 20 22 require ( 23 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 24 + github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect 25 + github.com/dustin/go-humanize v1.0.1 // indirect 26 + github.com/gogo/protobuf v1.3.2 // indirect 27 + github.com/google/flatbuffers v25.2.10+incompatible // indirect 28 + github.com/hashicorp/golang-lru v1.0.2 // indirect 29 + github.com/ipfs/bbloom v0.0.4 // indirect 30 + github.com/ipfs/go-block-format v0.2.0 // indirect 31 + github.com/ipfs/go-blockservice v0.5.2 // indirect 32 + github.com/ipfs/go-datastore v0.6.0 // indirect 33 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 34 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 35 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 36 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 37 + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 38 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 39 + github.com/ipfs/go-ipld-legacy v0.2.1 // indirect 40 + github.com/ipfs/go-log v1.0.5 // indirect 41 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 42 + github.com/ipfs/go-merkledag v0.11.0 // indirect 43 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 44 + github.com/ipfs/go-verifcid v0.0.3 // indirect 45 + github.com/ipld/go-car v0.6.2 // indirect 46 + github.com/ipld/go-codec-dagpb v1.6.0 // indirect 47 + github.com/ipld/go-ipld-prime v0.21.0 // indirect 48 + github.com/jbenet/goprocess v0.1.4 // indirect 49 + github.com/lestrrat-go/blackmagic v1.0.1 // indirect 50 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 51 + github.com/lestrrat-go/httprc v1.0.4 // indirect 52 + github.com/lestrrat-go/iter v1.0.2 // indirect 53 + github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect 54 + github.com/lestrrat-go/option v1.0.1 // indirect 55 + github.com/opentracing/opentracing-go v1.2.0 // indirect 56 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 57 + github.com/segmentio/asm v1.2.0 // indirect 58 + go.uber.org/atomic v1.11.0 // indirect 59 + go.uber.org/multierr v1.11.0 // indirect 60 + go.uber.org/zap v1.26.0 // indirect 61 + ) 62 + 63 + require ( 21 64 github.com/beorn7/perks v1.0.1 // indirect 65 + github.com/bluesky-social/jetstream v0.0.0-20251009222037-7d7efa58d7f1 22 66 github.com/bytedance/sonic v1.14.0 // indirect 23 67 github.com/bytedance/sonic/loader v0.3.0 // indirect 24 68 github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 69 github.com/cloudwego/base64x v0.1.6 // indirect 70 + github.com/dgraph-io/badger/v4 v4.8.0 26 71 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 27 72 github.com/felixge/httpsnoop v1.0.4 // indirect 28 - github.com/gabriel-vasile/mimetype v1.4.8 // indirect 73 + github.com/gabriel-vasile/mimetype v1.4.9 // indirect 29 74 github.com/gin-contrib/sse v1.1.0 // indirect 30 75 github.com/go-logr/logr v1.4.3 // indirect 31 76 github.com/go-logr/stdr v1.2.2 // indirect 32 77 github.com/go-playground/locales v0.14.1 // indirect 33 78 github.com/go-playground/universal-translator v0.18.1 // indirect 34 79 github.com/go-playground/validator/v10 v10.27.0 // indirect 35 - github.com/goccy/go-json v0.10.2 // indirect 80 + github.com/goccy/go-json v0.10.5 // indirect 36 81 github.com/goccy/go-yaml v1.18.0 // indirect 37 82 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 38 - github.com/ipfs/go-cid v0.4.1 // indirect 83 + github.com/ipfs/go-cid v0.5.0 39 84 github.com/json-iterator/go v1.1.12 // indirect 40 85 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 41 86 github.com/leodido/go-urn v1.4.0 // indirect ··· 59 104 github.com/spaolacci/murmur3 v1.1.0 // indirect 60 105 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 61 106 github.com/ugorji/go/codec v1.3.0 // indirect 62 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 107 + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect 108 + github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 63 109 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 64 110 go.opentelemetry.io/auto/sdk v1.1.0 // indirect 65 111 go.opentelemetry.io/otel/metric v1.38.0 // indirect ··· 74 120 golang.org/x/sys v0.35.0 // indirect 75 121 golang.org/x/text v0.28.0 // indirect 76 122 golang.org/x/tools v0.35.0 // indirect 77 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 123 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 78 124 google.golang.org/protobuf v1.36.9 // indirect 79 - lukechampine.com/blake3 v1.2.1 // indirect 125 + lukechampine.com/blake3 v1.4.1 // indirect 80 126 )
+295 -11
go.sum
··· 1 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 + github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 4 + github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 1 5 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 6 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 7 github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc h1:2t+uAvfzJiCsTMwn5fW85t/IGa0+2I7BXS2ORastK4o= 4 8 github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= 9 + github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635 h1:kNeRrgGJH2g5OvjLqtaQ744YXqduliZYpFkJ/ld47c0= 10 + github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= 11 + github.com/bluesky-social/jetstream v0.0.0-20251009222037-7d7efa58d7f1 h1:ovcRKN1iXZnY5WApVg+0Hw2RkwMH0ziA7lSAA8vellU= 12 + github.com/bluesky-social/jetstream v0.0.0-20251009222037-7d7efa58d7f1/go.mod h1:5PtGi4r/PjEVBBl+0xWuQn4mBEjr9h6xsfDBADS6cHs= 5 13 github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= 6 14 github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= 7 15 github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= ··· 10 18 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 19 github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 12 20 github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 21 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 22 + github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 23 + github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 13 24 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 25 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 26 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 + github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 28 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 29 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 30 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 31 + github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= 32 + github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= 33 + github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= 34 + github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= 35 + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 36 + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 16 37 github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 17 38 github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 18 39 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 h1:puGwrNTY2vCt8eakkSEq2yeNxUD3zb2kPhv1OsF1hPs= 19 40 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2/go.mod h1:ntxzdN7EhBp8h+N78AtN2hjbVKHa7mijryYd9nPMyMo= 20 41 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 21 42 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 22 - github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 23 - github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 43 + github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 44 + github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 45 + github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 46 + github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 47 + github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= 48 + github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= 24 49 github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 25 50 github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 26 51 github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= ··· 38 63 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 39 64 github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 40 65 github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 41 - github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 66 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 42 67 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 68 + github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 69 + github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 43 70 github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 44 71 github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 72 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 73 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 45 74 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 46 75 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 76 + github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 77 + github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 47 78 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 48 79 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 49 80 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 + github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 82 + github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 83 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 50 84 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 51 85 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 86 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 87 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 52 88 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 53 89 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 90 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 91 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 54 92 github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= 55 93 github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= 56 94 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 57 95 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 58 - github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 59 - github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 96 + github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= 97 + github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= 98 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 99 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 100 + github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= 101 + github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= 102 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 103 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 104 + github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= 105 + github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= 106 + github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 107 + github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 108 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 109 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 110 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 111 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 112 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 113 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 114 + github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= 115 + github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= 116 + github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 117 + github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 118 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 119 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 120 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= 121 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= 122 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= 123 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= 124 + github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY= 125 + github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= 126 + github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= 127 + github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= 128 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 129 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 130 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 131 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 132 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 133 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 134 + github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= 135 + github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= 136 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 137 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 138 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 139 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 140 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 141 + github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= 142 + github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= 143 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 144 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 145 + github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU= 146 + github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM= 147 + github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= 148 + github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= 149 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 h1:oFo19cBmcP0Cmg3XXbrr0V/c+xU9U1huEZp8+OgBzdI= 150 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4/go.mod h1:6nkFF8OmR5wLKBzRKi7/YFJpyYR7+oEn1DX+mMWnlLA= 151 + github.com/ipld/go-car v0.6.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc= 152 + github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8= 153 + github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= 154 + github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= 155 + github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 156 + github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 157 + github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 158 + github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 159 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 160 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 161 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 60 162 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 61 163 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 164 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 165 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 166 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 167 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 168 + github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= 169 + github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= 62 170 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 63 171 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 172 + github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= 173 + github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= 174 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 64 175 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 65 176 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 177 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 178 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 66 179 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 180 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 68 181 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 69 182 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 183 + github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= 184 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 185 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 186 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 187 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 188 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 189 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 190 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 191 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 192 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 193 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 194 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 195 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 196 + github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 197 + github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 198 + github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= 199 + github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= 200 + github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= 201 + github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= 202 + github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= 203 + github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= 204 + github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= 205 + github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= 206 + github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 207 + github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 208 + github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= 209 + github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= 210 + github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= 211 + github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= 212 + github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= 213 + github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= 214 + github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= 215 + github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= 216 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 70 217 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 71 218 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 219 + github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 220 + github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 221 + github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 222 + github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 72 223 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 73 224 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 74 225 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= ··· 82 233 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 83 234 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 84 235 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 236 + github.com/multiformats/go-multiaddr v0.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc= 237 + github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= 238 + github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= 239 + github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= 240 + github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 241 + github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 85 242 github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 86 243 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 244 + github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 245 + github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 87 246 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 88 247 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 248 + github.com/multiformats/go-multistream v0.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o= 249 + github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= 89 250 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 90 251 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 91 252 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 92 253 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 254 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 255 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 93 256 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 94 257 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 258 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 95 259 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 96 260 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 261 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 262 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 97 263 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 98 264 github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 99 265 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= ··· 106 272 github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 107 273 github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= 108 274 github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 275 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 109 276 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 110 277 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 278 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 279 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 280 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 281 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 282 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 283 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 284 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 285 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 286 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= 287 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= 111 288 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 112 289 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 113 290 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 291 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 115 292 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 116 293 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 294 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 295 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 296 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 117 297 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 118 298 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 119 299 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 300 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 120 301 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 121 302 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 122 303 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 123 304 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 124 305 github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 125 306 github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 126 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 127 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 307 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 308 + github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 309 + github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= 310 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 311 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 312 + github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 313 + github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 314 + github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 h1:W4jEGWdes35iuiiAYNZFOjx+dwzQOBh33kVpc0C0YiE= 315 + github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s= 316 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 317 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 318 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 319 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 128 320 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 129 321 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 130 322 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= ··· 143 335 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 144 336 go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 145 337 go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 338 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 339 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 340 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 341 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 342 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 146 343 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 147 344 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 148 345 go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 149 346 go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 347 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 348 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 349 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 350 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 351 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 352 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 353 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 354 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 355 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 150 356 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 151 357 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 152 358 golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 153 359 golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 360 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 361 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 362 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 363 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 364 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 365 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 154 366 golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 155 367 golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 368 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 369 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 370 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 371 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 372 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 373 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 374 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 156 375 golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= 157 376 golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 377 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 378 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 379 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 380 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 381 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 382 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 383 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 384 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 385 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 386 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 158 387 golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 159 388 golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 389 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 390 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 391 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 392 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 393 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 394 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 395 golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 161 396 golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 397 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 398 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 403 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 404 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 405 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 406 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 407 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 408 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 409 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 410 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 411 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 412 golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 164 413 golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 414 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 415 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 416 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 417 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 418 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 419 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 420 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 421 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 422 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 423 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 424 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 165 425 golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 166 426 golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 167 427 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 168 428 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 429 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 430 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 431 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 432 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 433 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 434 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 435 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 436 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 437 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 438 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 439 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 440 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 169 441 golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= 170 442 golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 171 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 172 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 443 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 444 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 445 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 446 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 447 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 448 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 173 449 google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 174 450 google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 175 451 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 452 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 176 453 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 177 454 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 455 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 456 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 457 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 458 + gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 459 + gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 178 460 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 461 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 462 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 180 463 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 - lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 182 - lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 464 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 465 + lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= 466 + lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
-81
main.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "log" 7 - "net/http" 8 - "os" 9 - "time" 10 - 11 - "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 12 - "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 13 - "tangled.org/whey.party/red-dwarf-server/sticket" 14 - 15 - // "github.com/bluesky-social/indigo/atproto/atclient" 16 - // comatproto "github.com/bluesky-social/indigo/api/atproto" 17 - // appbsky "github.com/bluesky-social/indigo/api/bsky" 18 - // "github.com/bluesky-social/indigo/atproto/atclient" 19 - // "github.com/bluesky-social/indigo/atproto/identity" 20 - // "github.com/bluesky-social/indigo/atproto/syntax" 21 - "github.com/bluesky-social/indigo/api/agnostic" 22 - // "github.com/bluesky-social/jetstream/pkg/models" 23 - ) 24 - 25 - const ( 26 - JETSTREAM_URL = "ws://localhost:6008/subscribe" 27 - SPACEDUST_URL = "ws://localhost:9998/subscribe" 28 - SLINGSHOT_URL = "http://localhost:7729" 29 - CONSTELLATION_URL = "http://localhost:7728" 30 - ) 31 - 32 - func main() { 33 - fmt.Fprintf(os.Stdout, "red-dwarf-server started") 34 - 35 - ctx := context.Background() 36 - mailbox := sticket.New() 37 - sl := slingshot.NewSlingshot(SLINGSHOT_URL) 38 - cs := constellation.NewConstellation(CONSTELLATION_URL) 39 - // spacedust is type definitions only 40 - // jetstream types is probably available from jetstream/pkg/models 41 - 42 - responsewow, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.profile", "did:plc:44ybard66vv44zksje25o7dz", "self") 43 - 44 - fmt.Fprintf(os.Stdout, responsewow.Uri) 45 - 46 - http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 47 - mailbox.HandleWS(&w, r) 48 - }) 49 - 50 - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 51 - fmt.Fprintf(w, "hello worldio !") 52 - clientUUID := sticket.GetUUIDFromRequest(r) 53 - hasSticket := clientUUID != "" 54 - if hasSticket { 55 - go func(targetUUID string) { 56 - // simulated heavy processing 57 - time.Sleep(2 * time.Second) 58 - 59 - lateData := map[string]any{ 60 - "postId": 101, 61 - "newComments": []string{ 62 - "Wow great tutorial!", 63 - "I am stuck on step 1.", 64 - }, 65 - } 66 - 67 - success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 68 - if success { 69 - log.Println("Successfully sent late data via Sticket") 70 - } else { 71 - log.Println("Failed to send late data (client disconnected?)") 72 - } 73 - }(clientUUID) 74 - } 75 - }) 76 - http.ListenAndServe(":7152", nil) 77 - } 78 - 79 - func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 80 - fmt.Fprintf(w, "hello worldio !") 81 - }
+40
public/getConfig.json
··· 1 + { 2 + "checkEmailConfirmed": true, 3 + "topicsEnabled": true, 4 + "liveNow": [ 5 + { 6 + "did": "did:plc:7sfnardo5xxznxc6esxc5ooe", 7 + "domains": [ 8 + "www.nba.com", 9 + "nba.com", 10 + "nba.smart.link", 11 + "espn.com", 12 + "www.espn.com" 13 + ] 14 + }, 15 + { "did": "did:plc:gx6fyi3jcfxd7ammq2t7mzp2", "domains": ["twitch.tv"] }, 16 + { "did": "did:plc:mc2hhszsfk6iwapfdwrwoj7i", "domains": ["twitch.tv"] }, 17 + { "did": "did:plc:sb54dpdfefflykmf5bcfvr7t", "domains": ["youtube.com"] }, 18 + { "did": "did:plc:dqav4a4ue5cyzgckv3l6fshr", "domains": ["twitch.tv"] }, 19 + { "did": "did:plc:q6f4hsgifb3qs2e3wwdycjtf", "domains": ["twitch.tv"] }, 20 + { "did": "did:plc:7tpvfx7mdbceyzj6kgboirle", "domains": ["twitch.tv"] }, 21 + { 22 + "did": "did:plc:4adlzwqtkv4dirxjwq4c3tlm", 23 + "domains": ["stream.place", "skylight.social"] 24 + }, 25 + { 26 + "did": "did:plc:2zmxikig2sj7gqaezl5gntae", 27 + "domains": ["stream.place", "skylight.social"] 28 + }, 29 + { 30 + "did": "did:plc:jsbkvuuviqj4xooqwcbaftav", 31 + "domains": ["stream.place", "skylight.social"] 32 + }, 33 + { 34 + "did": "did:plc:76iqtegcbbr4pbcxomka5pat", 35 + "domains": ["stream.place", "skylight.social"] 36 + }, 37 + { "did": "did:plc:r2mpjf3gz2ygfaodkzzzfddg", "domains": ["youtube.com"] }, 38 + { "did": "did:plc:wvwvlbeizca367klhhyudf2n", "domains": ["youtube.com"] } 39 + ] 40 + }
+66
readme.md
··· 1 + # Red Dwarf Server 2 + 3 + Red Dwarf but as a Go AppView server instead of a React TypeScript SPA 4 + 5 + you can use it right now by using `did:web:server.reddwarf.app` with any bluesky client that supports custom appviews (like for example [witchsky](https://witchsky.app/)) 6 + 7 + still very early in development 8 + 9 + implemented routes: 10 + - `app.bsky.actor.getProfiles` 11 + - `app.bsky.actor.getProfile` 12 + - `app.bsky.notification.listNotifications` (placeholder) 13 + - `app.bsky.labeler.getServices` 14 + - `app.bsky.feed.getFeedGenerators` 15 + - `app.bsky.feed.getPosts` (post rendering is incomplete) 16 + - `app.bsky.feed.getFeed` (post rendering is incomplete) 17 + - `app.bsky.unspecced.getConfig` (placeholder) 18 + - `app.bsky.unspecced.getPostThreadV2` (mostly working! doesnt use prefered sort, not performant yet) 19 + 20 + > [!NOTE] 21 + > uh im not very confident with the current directory structure, so files and folders might move around 22 + 23 + ## Runnables 24 + run all of these using `go run .` inside the respective directories 25 + 26 + ### `/cmd/appview` 27 + the main entry point, the actual appview itself. the api server that implements app.bsky.* XRPC methods 28 + 29 + ### `/cmd/backstream` 30 + experimental backfiller that kinda (but not really) conforms to the jetstream event shape. designed to be ingested by consumers expecting jetstream 31 + 32 + ### `/cmd/aturilist` 33 + experimental listRecords replacement. is not backfilled. uses the official jetstream go client, which means it suffers from this [bug](https://github.com/bluesky-social/jetstream/pull/45) 34 + 35 + ## Packages 36 + 37 + ### `/auth` 38 + taken from [go-bsky-feed-generator](https://github.com/jazware/go-bsky-feed-generator) but modified a bit. 39 + 40 + handles all of the auth, modified to have a more lenient version to make `getFeed` work 41 + 42 + ### `/microcosm/*` 43 + microcosm api clients, implements constellation slingshot and spacedust 44 + 45 + slingshot's api client is compatible with `github.com/bluesky-social/indigo/*` stuff, like `agnostic.RepoGetRecord` and `util.LexClient` 46 + 47 + ### `/shims/*` 48 + 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` 49 + 50 + 51 + ### `/sticket` 52 + unused leftover sorry 53 + 54 + 55 + ### `/store` 56 + unused leftover sorry 57 + 58 + ## todo 59 + 60 + - clean up /cmd/appview/main.go , its a mess 61 + - appview-side query caches 62 + - notification service 63 + - bookmarks service 64 + - create aturilist service 65 + - make backstream usable 66 + - create jetrelay service
+224
shims/lex/app/bsky/actor/defs/profileview.go
··· 1 + package appbskyactordefs 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + 7 + "github.com/bluesky-social/indigo/api/agnostic" 8 + appbsky "github.com/bluesky-social/indigo/api/bsky" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "tangled.org/whey.party/red-dwarf-server/microcosm" 11 + "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 12 + "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 13 + "tangled.org/whey.party/red-dwarf-server/shims/utils" 14 + ) 15 + 16 + func ProfileViewBasic(ctx context.Context, did utils.DID, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID) (*appbsky.ActorDefs_ProfileViewBasic, *appbsky.ActorProfile, error) { 17 + profileview, profile, err := ProfileView(ctx, did, sl, cs, imgcdn, viewer) 18 + 19 + if err != nil { 20 + return nil, nil, err 21 + } 22 + if profileview == nil { 23 + return nil, nil, nil 24 + } 25 + 26 + return &appbsky.ActorDefs_ProfileViewBasic{ 27 + Associated: profileview.Associated, 28 + Avatar: profileview.Avatar, 29 + CreatedAt: profileview.CreatedAt, 30 + Debug: profileview.Debug, 31 + Did: profileview.Did, 32 + DisplayName: profileview.DisplayName, 33 + Handle: profileview.Handle, 34 + Labels: profileview.Labels, 35 + Pronouns: profileview.Pronouns, 36 + Status: profileview.Status, 37 + Verification: profileview.Verification, 38 + Viewer: profileview.Viewer, 39 + }, profile, err 40 + } 41 + 42 + func ProfileView(ctx context.Context, did utils.DID, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID) (*appbsky.ActorDefs_ProfileView, *appbsky.ActorProfile, error) { 43 + identity, err_i := slingshot.ResolveMiniDoc(ctx, sl, string(did)) 44 + if err_i != nil { 45 + identity = nil 46 + } 47 + profilerecord, err_r := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", string(did), "self") 48 + if err_r != nil { 49 + return nil, nil, err_r 50 + } 51 + 52 + var profile appbsky.ActorProfile 53 + if err := json.Unmarshal(*profilerecord.Value, &profile); err != nil { 54 + return nil, nil, err 55 + } 56 + 57 + var handle string 58 + if identity != nil { 59 + handle = identity.Handle 60 + } else { 61 + handle = string(did) 62 + } 63 + 64 + var displayName string 65 + if profile.DisplayName != nil { 66 + displayName = *profile.DisplayName 67 + } else { 68 + if handle != "" { 69 + displayName = handle 70 + } else { 71 + displayName = string(did) 72 + } 73 + } 74 + var avatar *string 75 + if profile.Avatar != nil { 76 + url := utils.MakeImageCDN(did, imgcdn, "avatar", profile.Avatar.Ref.String()) 77 + avatar = &url 78 + } 79 + 80 + var blocking *string 81 + blockedBy := false 82 + var following *string 83 + var followedBy *string 84 + 85 + viewerProfileDID := "" 86 + if viewer != nil { 87 + viewerProfileDID = string(*viewer) 88 + } 89 + targetProfileDID := string(did) 90 + //viewerProfileURI, err_viewerProfileURI := syntax.ParseATURI("at://" + viewerProfileDID + "/app.bsky.actor.profile/self") 91 + //targetProfileURI, err_targetProfileURI := syntax.ParseATURI("at://" + targetProfileDID + "/app.bsky.actor.profile/self") 92 + 93 + //log.Println("viewerProfileDID: " + viewerProfileDID + " and targetProfileDID: " + targetProfileDID) 94 + 95 + if viewerProfileDID != "" && targetProfileDID != "" { 96 + blockingBacklink, err_blockingBacklink := constellation.GetBacklinks(ctx, cs, targetProfileDID, "app.bsky.graph.block:subject", []string{viewerProfileDID}, nil, nil) 97 + if err_blockingBacklink == nil && blockingBacklink.Records != nil && len(blockingBacklink.Records) > 0 && blockingBacklink.Total > 0 { 98 + blockingATURI, err_blockingATURI := syntax.ParseATURI("at://" + blockingBacklink.Records[0].Did + "/app.bsky.graph.block/" + blockingBacklink.Records[0].Rkey) 99 + if err_blockingATURI == nil { 100 + blockingString := blockingATURI.String() 101 + blocking = &blockingString 102 + } 103 + } 104 + blockedByBacklink, err_blockedByBacklink := constellation.GetBacklinks(ctx, cs, viewerProfileDID, "app.bsky.graph.block:subject", []string{targetProfileDID}, nil, nil) 105 + if err_blockedByBacklink == nil && blockedByBacklink.Records != nil && len(blockedByBacklink.Records) > 0 && blockedByBacklink.Total > 0 { 106 + _, err_blockedByATURI := syntax.ParseATURI("at://" + blockedByBacklink.Records[0].Did + "/app.bsky.graph.block/" + blockedByBacklink.Records[0].Rkey) 107 + if err_blockedByATURI == nil { 108 + blockedBy = true 109 + } 110 + } 111 + followingBacklink, err_followingBacklink := constellation.GetBacklinks(ctx, cs, targetProfileDID, "app.bsky.graph.follow:subject", []string{viewerProfileDID}, nil, nil) 112 + if err_followingBacklink == nil && followingBacklink.Records != nil && len(followingBacklink.Records) > 0 && followingBacklink.Total > 0 { 113 + followingATURI, err_followingATURI := syntax.ParseATURI("at://" + followingBacklink.Records[0].Did + "/app.bsky.graph.follow/" + followingBacklink.Records[0].Rkey) 114 + if err_followingATURI == nil { 115 + followingString := followingATURI.String() 116 + following = &followingString 117 + } 118 + } 119 + followedByBacklink, err_followedByBacklink := constellation.GetBacklinks(ctx, cs, viewerProfileDID, "app.bsky.graph.follow:subject", []string{targetProfileDID}, nil, nil) 120 + if err_followedByBacklink == nil && followedByBacklink.Records != nil && len(followedByBacklink.Records) > 0 && followedByBacklink.Total > 0 { 121 + followedByATURI, err_followedByATURI := syntax.ParseATURI("at://" + followedByBacklink.Records[0].Did + "/app.bsky.graph.follow/" + followedByBacklink.Records[0].Rkey) 122 + if err_followedByATURI == nil { 123 + followedByString := followedByATURI.String() 124 + followedBy = &followedByString 125 + } 126 + } 127 + 128 + } 129 + 130 + // we dont know before hand, so to make it visible on the page, we must set it to be non zero 131 + nonzeroassociated := int64(1) 132 + 133 + return &appbsky.ActorDefs_ProfileView{ 134 + Associated: &appbsky.ActorDefs_ProfileAssociated{ 135 + // ActivitySubscription *ActorDefs_ProfileAssociatedActivitySubscription `json:"activitySubscription,omitempty" cborgen:"activitySubscription,omitempty"` 136 + // Chat *ActorDefs_ProfileAssociatedChat `json:"chat,omitempty" cborgen:"chat,omitempty"` 137 + // Feedgens *int64 `json:"feedgens,omitempty" cborgen:"feedgens,omitempty"` 138 + Feedgens: &nonzeroassociated, 139 + // Labeler *bool `json:"labeler,omitempty" cborgen:"labeler,omitempty"` 140 + // Lists *int64 `json:"lists,omitempty" cborgen:"lists,omitempty"` 141 + Lists: &nonzeroassociated, 142 + // StarterPacks *int64 `json:"starterPacks,omitempty" cborgen:"starterPacks,omitempty"` 143 + StarterPacks: &nonzeroassociated, 144 + }, 145 + Avatar: avatar, 146 + CreatedAt: profile.CreatedAt, 147 + Debug: nil, 148 + Description: profile.Description, 149 + Did: string(did), 150 + DisplayName: &displayName, 151 + Handle: handle, 152 + IndexedAt: profile.CreatedAt, 153 + Labels: nil, 154 + Pronouns: nil, 155 + Status: nil, 156 + Verification: nil, 157 + Viewer: &appbsky.ActorDefs_ViewerState{ 158 + // // activitySubscription: This property is present only in selected cases, as an optimization. 159 + // ActivitySubscription *NotificationDefs_ActivitySubscription `json:"activitySubscription,omitempty" cborgen:"activitySubscription,omitempty"` 160 + // BlockedBy *bool `json:"blockedBy,omitempty" cborgen:"blockedBy,omitempty"` 161 + BlockedBy: &blockedBy, 162 + // Blocking *string `json:"blocking,omitempty" cborgen:"blocking,omitempty"` 163 + Blocking: blocking, 164 + // BlockingByList *GraphDefs_ListViewBasic `json:"blockingByList,omitempty" cborgen:"blockingByList,omitempty"` 165 + // FollowedBy *string `json:"followedBy,omitempty" cborgen:"followedBy,omitempty"` 166 + FollowedBy: followedBy, 167 + // Following *string `json:"following,omitempty" cborgen:"following,omitempty"` 168 + Following: following, 169 + // // knownFollowers: This property is present only in selected cases, as an optimization. 170 + // KnownFollowers *ActorDefs_KnownFollowers `json:"knownFollowers,omitempty" cborgen:"knownFollowers,omitempty"` 171 + // Muted *bool `json:"muted,omitempty" cborgen:"muted,omitempty"` 172 + // MutedByList *GraphDefs_ListViewBasic `json:"mutedByList,omitempty" cborgen:"mutedByList,omitempty"` 173 + }, 174 + }, &profile, nil 175 + } 176 + 177 + func ProfileViewDetailed(ctx context.Context, did utils.DID, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID) (*appbsky.ActorDefs_ProfileViewDetailed, *appbsky.ActorProfile, error) { 178 + profileview, profile, err := ProfileView(ctx, did, sl, cs, imgcdn, viewer) 179 + if err != nil { 180 + return nil, nil, err 181 + } 182 + followerCount_Out, err_i := constellation.LegacyLinksCountDistinctDids(ctx, cs, string(did), "app.bsky.graph.follow", ".subject", nil) 183 + if err_i != nil { 184 + followerCount_Out = nil 185 + } 186 + var followerCount int64 187 + if followerCount_Out == nil { 188 + followerCount = int64(-1) 189 + } else { 190 + followerCount = int64(followerCount_Out.Total) 191 + } 192 + 193 + var banner *string 194 + if profile.Banner != nil { 195 + url := utils.MakeImageCDN(did, imgcdn, "banner", profile.Banner.Ref.String()) 196 + banner = &url 197 + } 198 + 199 + nilCount := int64(-1) 200 + 201 + return &appbsky.ActorDefs_ProfileViewDetailed{ 202 + Associated: profileview.Associated, 203 + Avatar: profileview.Avatar, 204 + Banner: banner, 205 + CreatedAt: profileview.CreatedAt, 206 + Debug: profileview.Debug, 207 + Description: profileview.Description, 208 + Did: profileview.Did, 209 + DisplayName: profileview.DisplayName, 210 + FollowersCount: &followerCount, 211 + FollowsCount: &nilCount, // hardcoded placeholder 212 + Handle: profileview.Handle, 213 + IndexedAt: profileview.IndexedAt, 214 + JoinedViaStarterPack: nil, // hardcoded placeholder 215 + Labels: profileview.Labels, 216 + PinnedPost: profile.PinnedPost, 217 + PostsCount: &nilCount, // hardcoded placeholder 218 + Pronouns: profileview.Pronouns, 219 + Status: profileview.Status, 220 + Verification: profileview.Verification, 221 + Viewer: profileview.Viewer, 222 + Website: profile.Website, 223 + }, profile, nil 224 + }
+553
shims/lex/app/bsky/feed/defs/embed.go
··· 1 + package appbskyfeeddefs 2 + 3 + import ( 4 + "context" 5 + "log" 6 + 7 + "github.com/bluesky-social/indigo/api/atproto" 8 + appbsky "github.com/bluesky-social/indigo/api/bsky" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "tangled.org/whey.party/red-dwarf-server/microcosm" 11 + appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs" 12 + "tangled.org/whey.party/red-dwarf-server/shims/utils" 13 + ) 14 + 15 + func notFoundRecordEmbed(uri string) *appbsky.EmbedRecord_View_Record { 16 + return &appbsky.EmbedRecord_View_Record{ 17 + EmbedRecord_ViewNotFound: &appbsky.EmbedRecord_ViewNotFound{ 18 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.record#viewNotFound"` 19 + LexiconTypeID: "app.bsky.embed.record#viewNotFound", 20 + // NotFound bool `json:"notFound" cborgen:"notFound"` 21 + NotFound: true, 22 + // Uri string `json:"uri" cborgen:"uri"` 23 + Uri: uri, 24 + }} 25 + } 26 + 27 + func PostView_Embed(ctx context.Context, postaturi string, feedPost *appbsky.FeedPost, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID, disableTripleNestedRecord int) (*appbsky.FeedDefs_PostView_Embed, error) { 28 + //log.Println("(PostView_Embed) hey its: " + postaturi + " at depth: " + fmt.Sprint(disableTripleNestedRecord)) 29 + if feedPost.Embed == nil { 30 + return nil, nil 31 + } 32 + 33 + aturi, err := syntax.ParseATURI(postaturi) 34 + if err != nil { 35 + return nil, err 36 + } 37 + 38 + // determine type 39 + if feedPost.Embed.EmbedImages != nil { 40 + embedImage := EmbedImagesViewExtractor(ctx, aturi, feedPost.Embed.EmbedImages, sl, cs, imgcdn, viewer) 41 + return embedImage, nil 42 + } 43 + if feedPost.Embed.EmbedVideo != nil { 44 + //return nil, nil 45 + videocdn := "https://video.bsky.app" // todo move this 46 + embedVideo := EmbedVideoViewExtractor(ctx, aturi, feedPost.Embed.EmbedVideo, sl, cs, imgcdn, videocdn, viewer) 47 + return embedVideo, nil 48 + //embedType = "EmbedVideo" 49 + // return &appbsky.FeedDefs_PostView_Embed{ 50 + // // EmbedImages_View *EmbedImages_View 51 + // // EmbedVideo_View *EmbedVideo_View 52 + // EmbedVideo_View: &appbsky.EmbedVideo_View{ 53 + // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.video#view"` 54 + // LexiconTypeID: "app.bsky.embed.video#view", 55 + // // Alt *string `json:"alt,omitempty" cborgen:"alt,omitempty"` 56 + // Alt: 57 + // // AspectRatio *EmbedDefs_AspectRatio `json:"aspectRatio,omitempty" cborgen:"aspectRatio,omitempty"` 58 + // // Cid string `json:"cid" cborgen:"cid"` 59 + // // Playlist string `json:"playlist" cborgen:"playlist"` 60 + // // Thumbnail *string `json:"thumbnail,omitempty" cborgen:"thumbnail,omitempty"` 61 + // }, 62 + // // EmbedExternal_View *EmbedExternal_View 63 + // // EmbedRecord_View *EmbedRecord_View 64 + // // EmbedRecordWithMedia_View *EmbedRecordWithMedia_View 65 + // }, nil 66 + } 67 + if feedPost.Embed.EmbedExternal != nil { 68 + embedExternal := EmbedExternalViewExtractor(ctx, aturi, feedPost.Embed.EmbedExternal, sl, cs, imgcdn, viewer) 69 + return embedExternal, nil 70 + } 71 + if feedPost.Embed.EmbedRecord != nil && disableTripleNestedRecord > 0 { 72 + //return nil, nil 73 + // sigh this is a big one 74 + //embedType = "EmbedRecord" 75 + 76 + /* 77 + const author: $Typed<AppBskyActorDefs.ProfileViewBasic> = asTyped({ 78 + $type: "app.bsky.actor.defs#profileViewBasic" as const, 79 + did: quotedIdentity.did, 80 + handle: quotedIdentity.handle, 81 + displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 82 + avatar: quotedProfile.value.avatar?.ref?.$link 83 + ? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 84 + : undefined, 85 + viewer: {}, 86 + labels: [], 87 + }); 88 + 89 + const viewRecord: $Typed<AppBskyEmbedRecord.ViewRecord> = asTyped({ 90 + $type: "app.bsky.embed.record#viewRecord" as const, 91 + uri: quotedPost.uri, 92 + cid: quotedPost.cid, 93 + author, 94 + value: quotedPost.value, 95 + indexedAt: quotedPost.value.createdAt, 96 + embeds: quotedPost.value.embed ? [quotedPost.value.embed] : undefined, 97 + }); 98 + */ 99 + 100 + var record *appbsky.EmbedRecord_View_Record = EmbedRecordViewExtractor(ctx, feedPost.Embed.EmbedRecord.Record, sl, cs, imgcdn, viewer, disableTripleNestedRecord) 101 + if record == nil { 102 + return nil, nil 103 + } 104 + 105 + return &appbsky.FeedDefs_PostView_Embed{ 106 + // EmbedImages_View *EmbedImages_View 107 + // EmbedVideo_View *EmbedVideo_View 108 + // EmbedExternal_View *EmbedExternal_View 109 + // EmbedRecord_View *EmbedRecord_View 110 + EmbedRecord_View: &appbsky.EmbedRecord_View{ 111 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.record#view"` 112 + LexiconTypeID: "app.bsky.embed.record#view", 113 + // Record *EmbedRecord_View_Record `json:"record" cborgen:"record"` 114 + Record: record, 115 + // Record: &appbsky.EmbedRecord_View_Record{ 116 + // // EmbedRecord_ViewRecord *EmbedRecord_ViewRecord 117 + // // EmbedRecord_ViewNotFound *EmbedRecord_ViewNotFound 118 + // // EmbedRecord_ViewBlocked *EmbedRecord_ViewBlocked 119 + // // EmbedRecord_ViewDetached *EmbedRecord_ViewDetached 120 + // // FeedDefs_GeneratorView *FeedDefs_GeneratorView 121 + // // GraphDefs_ListView *GraphDefs_ListView 122 + // // LabelerDefs_LabelerView *LabelerDefs_LabelerView 123 + // // GraphDefs_StarterPackViewBasic *GraphDefs_StarterPackViewBasic 124 + // }, 125 + }, 126 + // EmbedRecordWithMedia_View *EmbedRecordWithMedia_View 127 + }, nil 128 + } 129 + if feedPost.Embed.EmbedRecordWithMedia != nil && disableTripleNestedRecord > 0 { 130 + //return nil, nil 131 + //embedType = "EmbedRecordWithMedia" 132 + 133 + var record *appbsky.EmbedRecord_View_Record = EmbedRecordViewExtractor(ctx, feedPost.Embed.EmbedRecordWithMedia.Record.Record, sl, cs, imgcdn, viewer, disableTripleNestedRecord) 134 + if record == nil { 135 + return nil, nil 136 + } 137 + var embedrecordview *appbsky.EmbedRecord_View 138 + if record.EmbedRecord_ViewRecord != nil { 139 + embedrecordview = &appbsky.EmbedRecord_View{ 140 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.record#view"` 141 + LexiconTypeID: "app.bsky.embed.record#view", 142 + // Record *EmbedRecord_View_Record `json:"record" cborgen:"record"` 143 + Record: record, 144 + } 145 + } 146 + 147 + var embedmediaview *appbsky.EmbedRecordWithMedia_View_Media 148 + 149 + if feedPost.Embed.EmbedRecordWithMedia.Media.EmbedImages != nil { 150 + embedImage := EmbedImagesViewExtractor(ctx, aturi, feedPost.Embed.EmbedRecordWithMedia.Media.EmbedImages, sl, cs, imgcdn, viewer) 151 + embedmediaview = &appbsky.EmbedRecordWithMedia_View_Media{ 152 + // EmbedImages_View *EmbedImages_View 153 + EmbedImages_View: &appbsky.EmbedImages_View{ 154 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.images#view"` 155 + LexiconTypeID: "app.bsky.embed.images#view", 156 + // Images []*EmbedImages_ViewImage `json:"images" cborgen:"images"` 157 + Images: embedImage.EmbedImages_View.Images, 158 + }, 159 + // EmbedVideo_View *EmbedVideo_View 160 + // EmbedExternal_View *EmbedExternal_View 161 + } 162 + } 163 + if feedPost.Embed.EmbedRecordWithMedia.Media.EmbedVideo != nil { 164 + videocdn := "https://video.bsky.app" // todo move this 165 + embedVideo := EmbedVideoViewExtractor(ctx, aturi, feedPost.Embed.EmbedVideo, sl, cs, imgcdn, videocdn, viewer) 166 + if embedVideo != nil { 167 + embedmediaview = &appbsky.EmbedRecordWithMedia_View_Media{ 168 + // EmbedImages_View *EmbedImages_View 169 + // EmbedVideo_View *EmbedVideo_View 170 + EmbedVideo_View: embedVideo.EmbedVideo_View, 171 + // EmbedVideo_View: &appbsky.EmbedVideo_View{ 172 + 173 + // }, 174 + // EmbedExternal_View *EmbedExternal_View 175 + } 176 + } 177 + // // video extractor 178 + // embedmediaview = &appbsky.EmbedRecordWithMedia_View_Media{ 179 + // // EmbedImages_View *EmbedImages_View 180 + // // EmbedVideo_View *EmbedVideo_View 181 + // // EmbedExternal_View *EmbedExternal_View 182 + // } 183 + } 184 + if feedPost.Embed.EmbedRecordWithMedia.Media.EmbedExternal != nil { 185 + embedExternal := EmbedExternalViewExtractor(ctx, aturi, feedPost.Embed.EmbedRecordWithMedia.Media.EmbedExternal, sl, cs, imgcdn, viewer) 186 + embedmediaview = &appbsky.EmbedRecordWithMedia_View_Media{ 187 + // EmbedImages_View *EmbedImages_View 188 + // EmbedVideo_View *EmbedVideo_View 189 + // EmbedExternal_View *EmbedExternal_View 190 + EmbedExternal_View: &appbsky.EmbedExternal_View{ 191 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.external#view"` 192 + LexiconTypeID: "app.bsky.embed.external#view", 193 + // External *EmbedExternal_ViewExternal `json:"external" cborgen:"external"` 194 + External: embedExternal.EmbedExternal_View.External, 195 + }, 196 + } 197 + } 198 + 199 + return &appbsky.FeedDefs_PostView_Embed{ 200 + // EmbedImages_View *EmbedImages_View 201 + // EmbedVideo_View *EmbedVideo_View 202 + // EmbedExternal_View *EmbedExternal_View 203 + // EmbedRecord_View *EmbedRecord_View 204 + // EmbedRecordWithMedia_View *EmbedRecordWithMedia_View 205 + EmbedRecordWithMedia_View: &appbsky.EmbedRecordWithMedia_View{ 206 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.recordWithMedia#view"` 207 + LexiconTypeID: "", 208 + // Media *EmbedRecordWithMedia_View_Media `json:"media" cborgen:"media"` 209 + Media: embedmediaview, 210 + // Record *EmbedRecord_View `json:"record" cborgen:"record"` 211 + Record: embedrecordview, 212 + }, 213 + }, nil 214 + } 215 + 216 + return nil, nil 217 + } 218 + 219 + func EmbedImagesViewExtractor(ctx context.Context, aturi syntax.ATURI, embedImages *appbsky.EmbedImages, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID) *appbsky.FeedDefs_PostView_Embed { 220 + //embedType = "EmbedImages" 221 + // thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 222 + // fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`, 223 + var images []*appbsky.EmbedImages_ViewImage 224 + for _, rawimg := range embedImages.Images { 225 + 226 + var feed_thumbnail string 227 + var feed_fullsize string 228 + if rawimg.Image != nil { 229 + u := utils.MakeImageCDN(utils.DID(aturi.Authority().String()), imgcdn, "feed_thumbnail", rawimg.Image.Ref.String()) 230 + feed_thumbnail = u 231 + uf := utils.MakeImageCDN(utils.DID(aturi.Authority().String()), imgcdn, "feed_fullsize", rawimg.Image.Ref.String()) 232 + feed_fullsize = uf 233 + } 234 + img := appbsky.EmbedImages_ViewImage{ 235 + // // alt: Alt text description of the image, for accessibility. 236 + // Alt string `json:"alt" cborgen:"alt"` 237 + Alt: rawimg.Alt, 238 + // AspectRatio *EmbedDefs_AspectRatio `json:"aspectRatio,omitempty" cborgen:"aspectRatio,omitempty"` 239 + AspectRatio: rawimg.AspectRatio, 240 + // // fullsize: Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. 241 + // Fullsize string `json:"fullsize" cborgen:"fullsize"` 242 + Fullsize: feed_fullsize, 243 + // // thumb: Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. 244 + // Thumb string `json:"thumb" cborgen:"thumb"` 245 + Thumb: feed_thumbnail, 246 + } 247 + images = append(images, &img) 248 + } 249 + return &appbsky.FeedDefs_PostView_Embed{ 250 + // EmbedImages_View *EmbedImages_View 251 + EmbedImages_View: &appbsky.EmbedImages_View{ 252 + LexiconTypeID: "app.bsky.embed.images#view", 253 + Images: images, 254 + }, 255 + // EmbedVideo_View *EmbedVideo_View 256 + // EmbedExternal_View *EmbedExternal_View 257 + // EmbedRecord_View *EmbedRecord_View 258 + // EmbedRecordWithMedia_View *EmbedRecordWithMedia_View 259 + } 260 + 261 + } 262 + 263 + func EmbedVideoViewExtractor(ctx context.Context, aturi syntax.ATURI, embedVideo *appbsky.EmbedVideo, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, videocdn string, viewer *utils.DID) *appbsky.FeedDefs_PostView_Embed { 264 + // u := utils.MakeImageCDN(utils.DID(aturi.Authority().String()), imgcdn, "feed_thumbnail", rawimg.Image.Ref.String()) 265 + // feed_thumbnail = u 266 + // uf := utils.MakeImageCDN(utils.DID(aturi.Authority().String()), imgcdn, "feed_fullsize", rawimg.Image.Ref.String()) 267 + // feed_fullsize = uf 268 + /* 269 + uri at://did:plc:mdjhvva6vlrswsj26cftjttd/app.bsky.feed.post/3m7lci6jy4k2m 270 + video cid "bafkreifqh5647m6rsmuxpajitmbjigkg5xdfl6p4v4losks76w77vvtau4" 271 + playlist "https://video.bsky.app/watch/did%3Aplc%3Amdjhvva6vlrswsj26cftjttd/bafkreifqh5647m6rsmuxpajitmbjigkg5xdfl6p4v4losks76w77vvtau4/playlist.m3u8" 272 + {videocdn}/watch/{uri encoded did}/{video cid}/playlist.m3u8 273 + thumbnail "https://video.bsky.app/watch/did%3Aplc%3Amdjhvva6vlrswsj26cftjttd/bafkreifqh5647m6rsmuxpajitmbjigkg5xdfl6p4v4losks76w77vvtau4/thumbnail.jpg" 274 + {videocdn}/watch/{uri encoded did}/{video cid}/thumbnail.jpg 275 + */ 276 + if embedVideo == nil || embedVideo.Video == nil { 277 + return nil 278 + } 279 + didstring := aturi.Authority().String() 280 + did := utils.DID(didstring) 281 + playlist := utils.MakeVideoCDN(did, videocdn, "playlist.m3u8", embedVideo.Video.Ref.String()) 282 + thumbnail := utils.MakeVideoCDN(did, videocdn, "thumbnail.jpg", embedVideo.Video.Ref.String()) 283 + return &appbsky.FeedDefs_PostView_Embed{ 284 + // EmbedImages_View *EmbedImages_View 285 + // EmbedVideo_View *EmbedVideo_View 286 + EmbedVideo_View: &appbsky.EmbedVideo_View{ 287 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.video#view"` 288 + LexiconTypeID: "app.bsky.embed.video#view", 289 + // Alt *string `json:"alt,omitempty" cborgen:"alt,omitempty"` 290 + Alt: embedVideo.Alt, 291 + // AspectRatio *EmbedDefs_AspectRatio `json:"aspectRatio,omitempty" cborgen:"aspectRatio,omitempty"` 292 + AspectRatio: embedVideo.AspectRatio, 293 + // Cid string `json:"cid" cborgen:"cid"` 294 + Cid: embedVideo.Video.Ref.String(), 295 + // Playlist string `json:"playlist" cborgen:"playlist"` 296 + Playlist: playlist, 297 + // Thumbnail *string `json:"thumbnail,omitempty" cborgen:"thumbnail,omitempty"` 298 + Thumbnail: &thumbnail, 299 + }, 300 + // EmbedExternal_View *EmbedExternal_View 301 + // EmbedRecord_View *EmbedRecord_View 302 + // EmbedRecordWithMedia_View *EmbedRecordWithMedia_View 303 + } 304 + 305 + } 306 + 307 + func EmbedExternalViewExtractor(ctx context.Context, aturi syntax.ATURI, embedExternal *appbsky.EmbedExternal, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID) *appbsky.FeedDefs_PostView_Embed { 308 + // todo: gif embeds needs special handling i think? maybe? 309 + //return nil, nil 310 + //embedType = "EmbedExternal" 311 + rawimg := embedExternal.External 312 + var thumbnail *string 313 + if rawimg.Thumb != nil { 314 + u := utils.MakeImageCDN(utils.DID(aturi.Authority().String()), imgcdn, "feed_thumbnail", rawimg.Thumb.Ref.String()) 315 + thumbnail = &u 316 + } 317 + return &appbsky.FeedDefs_PostView_Embed{ 318 + // EmbedImages_View *EmbedImages_View 319 + // EmbedVideo_View *EmbedVideo_View 320 + // EmbedExternal_View *EmbedExternal_View 321 + EmbedExternal_View: &appbsky.EmbedExternal_View{ 322 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.external#view"` 323 + LexiconTypeID: "app.bsky.embed.external#view", 324 + // External *EmbedExternal_ViewExternal `json:"external" cborgen:"external"` 325 + External: &appbsky.EmbedExternal_ViewExternal{ 326 + // Description string `json:"description" cborgen:"description"` 327 + Description: embedExternal.External.Description, 328 + // Thumb *string `json:"thumb,omitempty" cborgen:"thumb,omitempty"` 329 + Thumb: thumbnail, 330 + // Title string `json:"title" cborgen:"title"` 331 + Title: embedExternal.External.Title, 332 + // Uri string `json:"uri" cborgen:"uri"` 333 + Uri: embedExternal.External.Uri, 334 + }, 335 + }, 336 + // EmbedRecord_View *EmbedRecord_View 337 + // EmbedRecordWithMedia_View *EmbedRecordWithMedia_View 338 + } 339 + } 340 + 341 + func EmbedRecordViewExtractor(ctx context.Context, record *atproto.RepoStrongRef, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID, disableTripleNestedRecord int) *appbsky.EmbedRecord_View_Record { 342 + if record == nil { 343 + log.Println("[EmbedRecord_View_Record] no record *(????)") 344 + return nil 345 + } 346 + raw := record.Uri 347 + aturi, err := syntax.ParseATURI(raw) 348 + if err != nil { 349 + log.Println("[EmbedRecord_View_Record] bad aturi") 350 + return nil 351 + } 352 + collection := aturi.Collection().String() 353 + 354 + if collection == "app.bsky.feed.post" { 355 + //t.EmbedRecord_ViewRecord.LexiconTypeID = "app.bsky.embed.record#viewRecord" 356 + profileViewBasic, _, err := appbskyactordefs.ProfileViewBasic(ctx, utils.DID(aturi.Authority().String()), sl, cs, imgcdn, viewer) 357 + if err != nil { 358 + log.Println("[EmbedRecord_View_Record] profileviewbasic failed") 359 + return notFoundRecordEmbed(aturi.String()) 360 + } 361 + 362 + postView, _, err := PostView(ctx, aturi.String(), sl, cs, imgcdn, viewer, disableTripleNestedRecord-1) 363 + if err != nil { 364 + log.Println("[EmbedRecord_View_Record] postview failed") 365 + return notFoundRecordEmbed(aturi.String()) 366 + } 367 + 368 + // postRecordResponse, err_r := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.post", aturi.Authority().String(), aturi.RecordKey().String()) 369 + // if err_r != nil { 370 + // return notFoundRecordEmbed(aturi.String()) 371 + // } 372 + // var postRecord appbsky.FeedPost 373 + // if err := json.Unmarshal(*postRecordResponse.Value, &postRecord); err != nil { 374 + // return notFoundRecordEmbed(aturi.String()) 375 + // } 376 + 377 + // lexicontypedecoder := &util.LexiconTypeDecoder{Val: &postRecord} 378 + //var has string /*image | video | external*/ 379 + 380 + var embeds []*appbsky.EmbedRecord_ViewRecord_Embeds_Elem 381 + if postView.Embed != nil { 382 + if postView.Embed.EmbedImages_View != nil { 383 + embeds = []*appbsky.EmbedRecord_ViewRecord_Embeds_Elem{ 384 + { 385 + EmbedImages_View: postView.Embed.EmbedImages_View, 386 + }, 387 + } 388 + } 389 + if postView.Embed.EmbedVideo_View != nil { 390 + //has = "video" 391 + embeds = []*appbsky.EmbedRecord_ViewRecord_Embeds_Elem{ 392 + { 393 + EmbedVideo_View: postView.Embed.EmbedVideo_View, 394 + }, 395 + } 396 + } 397 + if postView.Embed.EmbedExternal_View != nil { 398 + embeds = []*appbsky.EmbedRecord_ViewRecord_Embeds_Elem{ 399 + { 400 + EmbedExternal_View: postView.Embed.EmbedExternal_View, 401 + }, 402 + } 403 + } 404 + } 405 + 406 + return &appbsky.EmbedRecord_View_Record{ 407 + // EmbedRecord_ViewRecord *EmbedRecord_ViewRecord 408 + EmbedRecord_ViewRecord: &appbsky.EmbedRecord_ViewRecord{ 409 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.record#viewRecord"` 410 + LexiconTypeID: "app.bsky.embed.record#viewRecord", 411 + // Author *ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"` 412 + Author: profileViewBasic, 413 + // Cid string `json:"cid" cborgen:"cid"` 414 + Cid: postView.Cid, 415 + // Embeds []*EmbedRecord_ViewRecord_Embeds_Elem `json:"embeds,omitempty" cborgen:"embeds,omitempty"` 416 + Embeds: embeds, 417 + // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 418 + IndexedAt: postView.IndexedAt, 419 + // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 420 + Labels: postView.Labels, 421 + // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 422 + LikeCount: postView.LikeCount, 423 + // QuoteCount *int64 `json:"quoteCount,omitempty" cborgen:"quoteCount,omitempty"` 424 + QuoteCount: postView.QuoteCount, 425 + // ReplyCount *int64 `json:"replyCount,omitempty" cborgen:"replyCount,omitempty"` 426 + ReplyCount: postView.ReplyCount, 427 + // RepostCount *int64 `json:"repostCount,omitempty" cborgen:"repostCount,omitempty"` 428 + RepostCount: postView.RepostCount, 429 + // Uri string `json:"uri" cborgen:"uri"` 430 + Uri: postView.Uri, 431 + // // value: The record data itself. 432 + // Value *lexutil.LexiconTypeDecoder `json:"value" cborgen:"value"` 433 + Value: postView.Record, 434 + }, 435 + // EmbedRecord_ViewNotFound *EmbedRecord_ViewNotFound 436 + // EmbedRecord_ViewBlocked *EmbedRecord_ViewBlocked 437 + // EmbedRecord_ViewDetached *EmbedRecord_ViewDetached 438 + // FeedDefs_GeneratorView *FeedDefs_GeneratorView 439 + // GraphDefs_ListView *GraphDefs_ListView 440 + // LabelerDefs_LabelerView *LabelerDefs_LabelerView 441 + // GraphDefs_StarterPackViewBasic *GraphDefs_StarterPackViewBasic 442 + } 443 + } 444 + if collection == "app.bsky.feed.generator" { 445 + return notFoundRecordEmbed(aturi.String()) 446 + return nil 447 + //t.FeedDefs_GeneratorView.LexiconTypeID = "app.bsky.feed.defs#generatorView" 448 + return &appbsky.EmbedRecord_View_Record{ 449 + // EmbedRecord_ViewRecord *EmbedRecord_ViewRecord 450 + // EmbedRecord_ViewNotFound *EmbedRecord_ViewNotFound 451 + // EmbedRecord_ViewBlocked *EmbedRecord_ViewBlocked 452 + // EmbedRecord_ViewDetached *EmbedRecord_ViewDetached 453 + // FeedDefs_GeneratorView *FeedDefs_GeneratorView 454 + FeedDefs_GeneratorView: &appbsky.FeedDefs_GeneratorView{ 455 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#generatorView"` 456 + // AcceptsInteractions *bool `json:"acceptsInteractions,omitempty" cborgen:"acceptsInteractions,omitempty"` 457 + // Avatar *string `json:"avatar,omitempty" cborgen:"avatar,omitempty"` 458 + // Cid string `json:"cid" cborgen:"cid"` 459 + // ContentMode *string `json:"contentMode,omitempty" cborgen:"contentMode,omitempty"` 460 + // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 461 + // Description *string `json:"description,omitempty" cborgen:"description,omitempty"` 462 + // DescriptionFacets []*RichtextFacet `json:"descriptionFacets,omitempty" cborgen:"descriptionFacets,omitempty"` 463 + // Did string `json:"did" cborgen:"did"` 464 + // DisplayName string `json:"displayName" cborgen:"displayName"` 465 + // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 466 + // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 467 + // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 468 + // Uri string `json:"uri" cborgen:"uri"` 469 + // Viewer *FeedDefs_GeneratorViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 470 + }, 471 + // GraphDefs_ListView *GraphDefs_ListView 472 + // LabelerDefs_LabelerView *LabelerDefs_LabelerView 473 + // GraphDefs_StarterPackViewBasic *GraphDefs_StarterPackViewBasic 474 + } 475 + } 476 + if collection == "app.bsky.graph.list" { 477 + return notFoundRecordEmbed(aturi.String()) 478 + return nil 479 + //t.GraphDefs_ListView.LexiconTypeID = "app.bsky.graph.defs#listView" 480 + return &appbsky.EmbedRecord_View_Record{ 481 + // EmbedRecord_ViewRecord *EmbedRecord_ViewRecord 482 + // EmbedRecord_ViewNotFound *EmbedRecord_ViewNotFound 483 + // EmbedRecord_ViewBlocked *EmbedRecord_ViewBlocked 484 + // EmbedRecord_ViewDetached *EmbedRecord_ViewDetached 485 + // FeedDefs_GeneratorView *FeedDefs_GeneratorView 486 + // GraphDefs_ListView *GraphDefs_ListView 487 + GraphDefs_ListView: &appbsky.GraphDefs_ListView{ 488 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.graph.defs#listView"` 489 + // Avatar *string `json:"avatar,omitempty" cborgen:"avatar,omitempty"` 490 + // Cid string `json:"cid" cborgen:"cid"` 491 + // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 492 + // Description *string `json:"description,omitempty" cborgen:"description,omitempty"` 493 + // DescriptionFacets []*RichtextFacet `json:"descriptionFacets,omitempty" cborgen:"descriptionFacets,omitempty"` 494 + // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 495 + // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 496 + // ListItemCount *int64 `json:"listItemCount,omitempty" cborgen:"listItemCount,omitempty"` 497 + // Name string `json:"name" cborgen:"name"` 498 + // Purpose *string `json:"purpose" cborgen:"purpose"` 499 + // Uri string `json:"uri" cborgen:"uri"` 500 + // Viewer *GraphDefs_ListViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 501 + }, 502 + // LabelerDefs_LabelerView *LabelerDefs_LabelerView 503 + // GraphDefs_StarterPackViewBasic *GraphDefs_StarterPackViewBasic 504 + } 505 + } 506 + // if t.LabelerDefs_LabelerView != nil { 507 + // t.LabelerDefs_LabelerView.LexiconTypeID = "app.bsky.labeler.defs#labelerView" 508 + // return json.Marshal(t.LabelerDefs_LabelerView) 509 + // } 510 + if collection == "app.bsky.graph.starterpack" { 511 + return notFoundRecordEmbed(aturi.String()) 512 + return nil 513 + //t.GraphDefs_StarterPackViewBasic.LexiconTypeID = "app.bsky.graph.defs#starterPackViewBasic" 514 + return &appbsky.EmbedRecord_View_Record{ 515 + // EmbedRecord_ViewRecord *EmbedRecord_ViewRecord 516 + // EmbedRecord_ViewNotFound *EmbedRecord_ViewNotFound 517 + // EmbedRecord_ViewBlocked *EmbedRecord_ViewBlocked 518 + // EmbedRecord_ViewDetached *EmbedRecord_ViewDetached 519 + // FeedDefs_GeneratorView *FeedDefs_GeneratorView 520 + // GraphDefs_ListView *GraphDefs_ListView 521 + // LabelerDefs_LabelerView *LabelerDefs_LabelerView 522 + // GraphDefs_StarterPackViewBasic *GraphDefs_StarterPackViewBasic 523 + GraphDefs_StarterPackViewBasic: &appbsky.GraphDefs_StarterPackViewBasic{ 524 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.graph.defs#starterPackViewBasic"` 525 + // Cid string `json:"cid" cborgen:"cid"` 526 + // Creator *ActorDefs_ProfileViewBasic `json:"creator" cborgen:"creator"` 527 + // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 528 + // JoinedAllTimeCount *int64 `json:"joinedAllTimeCount,omitempty" cborgen:"joinedAllTimeCount,omitempty"` 529 + // JoinedWeekCount *int64 `json:"joinedWeekCount,omitempty" cborgen:"joinedWeekCount,omitempty"` 530 + // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 531 + // ListItemCount *int64 `json:"listItemCount,omitempty" cborgen:"listItemCount,omitempty"` 532 + // Record *lexutil.LexiconTypeDecoder `json:"record" cborgen:"record"` 533 + // Uri string `json:"uri" cborgen:"uri"` 534 + }, 535 + } 536 + } 537 + 538 + // if t.EmbedRecord_ViewNotFound != nil { 539 + // t.EmbedRecord_ViewNotFound.LexiconTypeID = "app.bsky.embed.record#viewNotFound" 540 + // return json.Marshal(t.EmbedRecord_ViewNotFound) 541 + // } 542 + // if t.EmbedRecord_ViewBlocked != nil { 543 + // t.EmbedRecord_ViewBlocked.LexiconTypeID = "app.bsky.embed.record#viewBlocked" 544 + // return json.Marshal(t.EmbedRecord_ViewBlocked) 545 + // } 546 + // if t.EmbedRecord_ViewDetached != nil { 547 + // t.EmbedRecord_ViewDetached.LexiconTypeID = "app.bsky.embed.record#viewDetached" 548 + // return json.Marshal(t.EmbedRecord_ViewDetached) 549 + // } 550 + return notFoundRecordEmbed(aturi.String()) 551 + return nil 552 + 553 + }
+229
shims/lex/app/bsky/feed/defs/postview.go
··· 1 + package appbskyfeeddefs 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + 7 + "github.com/bluesky-social/indigo/api/agnostic" 8 + comatproto "github.com/bluesky-social/indigo/api/atproto" 9 + appbsky "github.com/bluesky-social/indigo/api/bsky" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "tangled.org/whey.party/red-dwarf-server/microcosm" 12 + "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 13 + 14 + //"tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 15 + //"tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 16 + "github.com/bluesky-social/indigo/lex/util" 17 + appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs" 18 + "tangled.org/whey.party/red-dwarf-server/shims/utils" 19 + ) 20 + 21 + func PostView(ctx context.Context, postaturi string, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, viewer *utils.DID, disableTripleNestedRecord int) (*appbsky.FeedDefs_PostView, *appbsky.FeedPost, error) { 22 + //log.Println("(PostView) hey its: " + postaturi + " at depth: " + fmt.Sprint(disableTripleNestedRecord)) 23 + aturi, err := syntax.ParseATURI(postaturi) 24 + if err != nil { 25 + return nil, nil, err 26 + } 27 + 28 + did := aturi.Authority().String() 29 + rkey := aturi.RecordKey().String() 30 + repoDID, err := utils.NewDID(did) 31 + if err != nil { 32 + return nil, nil, err 33 + } 34 + 35 + postRecordResponse, err_r := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.post", string(did), rkey) 36 + if err_r != nil { 37 + return nil, nil, err_r 38 + } 39 + 40 + var postRecord appbsky.FeedPost 41 + if err := json.Unmarshal(*postRecordResponse.Value, &postRecord); err != nil { 42 + return nil, nil, err 43 + } 44 + var postView_Embed *appbsky.FeedDefs_PostView_Embed 45 + //if !disableTripleNestedRecord { 46 + postView_Embed, err = PostView_Embed(ctx, postaturi, &postRecord, sl, cs, imgcdn, viewer, disableTripleNestedRecord) 47 + if err != nil { 48 + postView_Embed = nil 49 + } 50 + //} 51 + 52 + profile, _, err := appbskyactordefs.ProfileViewBasic(ctx, repoDID, sl, cs, imgcdn, viewer) 53 + if err != nil || profile == nil { 54 + if profile == nil { 55 + //log.Println("WHAT!! profile / author field is null?!?!?! whyyy") 56 + //log.Println(err) 57 + } 58 + return nil, nil, err 59 + } 60 + 61 + lexicontypedecoder := &util.LexiconTypeDecoder{Val: &postRecord} 62 + 63 + fakeCount := int64(-1) 64 + links, err := constellation.LegacyLinksAll(ctx, cs, postRecordResponse.Uri) 65 + var likeCount int64 66 + var repostCount int64 67 + var replyCount int64 68 + var quoteCount_noEmbed int64 69 + var quoteCount_withEmbed int64 70 + var quoteCount int64 71 + if err == nil { 72 + if links != nil && 73 + links.Links != nil { 74 + like, ok := links.Links["app.bsky.feed.like"] 75 + if ok && like != nil { 76 + subj, ok := like[".subject.uri"] 77 + if ok { 78 + likeCount = int64(subj.Records) 79 + } else { 80 + likeCount = int64(0) 81 + } 82 + } else { 83 + likeCount = int64(0) 84 + } 85 + } 86 + if links != nil && 87 + links.Links != nil { 88 + like, ok := links.Links["app.bsky.feed.repost"] 89 + if ok && like != nil { 90 + subj, ok := like[".subject.uri"] 91 + if ok { 92 + repostCount = int64(subj.Records) 93 + } else { 94 + repostCount = int64(0) 95 + } 96 + } else { 97 + repostCount = int64(0) 98 + } 99 + } 100 + if links != nil && 101 + links.Links != nil { 102 + like, ok := links.Links["app.bsky.feed.post"] 103 + if ok && like != nil { 104 + subj, ok := like[".reply.parent.uri"] 105 + if ok { 106 + replyCount = int64(subj.Records) 107 + } else { 108 + replyCount = int64(0) 109 + } 110 + } else { 111 + replyCount = int64(0) 112 + } 113 + } 114 + if links != nil && 115 + links.Links != nil { 116 + like, ok := links.Links["app.bsky.feed.post"] 117 + if ok && like != nil { 118 + subj, ok := like[".embed.record.uri"] 119 + if ok { 120 + quoteCount_noEmbed = int64(subj.Records) 121 + } else { 122 + quoteCount_noEmbed = int64(0) 123 + } 124 + } else { 125 + quoteCount_noEmbed = int64(0) 126 + } 127 + } 128 + if links != nil && 129 + links.Links != nil { 130 + like, ok := links.Links["app.bsky.feed.post"] 131 + if ok && like != nil { 132 + subj, ok := like[".embed.record.record.uri"] 133 + if ok { 134 + quoteCount_withEmbed = int64(subj.Records) 135 + } else { 136 + quoteCount_withEmbed = int64(0) 137 + } 138 + } else { 139 + quoteCount_withEmbed = int64(0) 140 + } 141 + } 142 + quoteCount = quoteCount_noEmbed + quoteCount_withEmbed 143 + } else { 144 + likeCount, repostCount, replyCount, quoteCount_noEmbed, quoteCount_withEmbed, quoteCount = 145 + fakeCount, fakeCount, fakeCount, fakeCount, fakeCount, fakeCount 146 + } 147 + 148 + var viewerState *appbsky.FeedDefs_ViewerState 149 + if viewer != nil { 150 + //log.Println("viewer is not nil: " + *viewer) 151 + did := []string{string(*viewer)} 152 + likeBacklinks, likeerr := constellation.GetBacklinks(ctx, cs, postaturi, "app.bsky.feed.like:subject.uri", did, nil, nil) 153 + repostBacklinks, reposterr := constellation.GetBacklinks(ctx, cs, postaturi, "app.bsky.feed.repost:subject.uri", did, nil, nil) 154 + if likeerr == nil && reposterr == nil { 155 + var likeATURI syntax.ATURI 156 + var repostATURI syntax.ATURI 157 + if likeBacklinks.Total > 0 { 158 + likeATURI, err = syntax.ParseATURI("at://" + likeBacklinks.Records[0].Did + "/" + likeBacklinks.Records[0].Collection + "/" + likeBacklinks.Records[0].Rkey) 159 + if err != nil { 160 + likeATURI = "" 161 + } 162 + } 163 + if repostBacklinks.Total > 0 { 164 + repostATURI, err = syntax.ParseATURI("at://" + repostBacklinks.Records[0].Did + "/" + repostBacklinks.Records[0].Collection + "/" + repostBacklinks.Records[0].Rkey) 165 + if err != nil { 166 + repostATURI = "" 167 + } 168 + } 169 + likelit := string(likeATURI) 170 + repostlit := string(repostATURI) 171 + var like *string = &likelit 172 + if likelit == "" { 173 + like = nil 174 + } 175 + var repost *string = &repostlit 176 + if repostlit == "" { 177 + repost = nil 178 + } 179 + viewerState = &appbsky.FeedDefs_ViewerState{ 180 + // Bookmarked *bool `json:"bookmarked,omitempty" cborgen:"bookmarked,omitempty"` 181 + // EmbeddingDisabled *bool `json:"embeddingDisabled,omitempty" cborgen:"embeddingDisabled,omitempty"` 182 + // Like *string `json:"like,omitempty" cborgen:"like,omitempty"` 183 + Like: like, 184 + // Pinned *bool `json:"pinned,omitempty" cborgen:"pinned,omitempty"` 185 + // ReplyDisabled *bool `json:"replyDisabled,omitempty" cborgen:"replyDisabled,omitempty"` 186 + // Repost *string `json:"repost,omitempty" cborgen:"repost,omitempty"` 187 + Repost: repost, 188 + // ThreadMuted *bool `json:"threadMuted,omitempty" cborgen:"threadMuted,omitempty"` 189 + } 190 + } 191 + } else { 192 + //log.Println("viewer is nil") 193 + } 194 + 195 + var emptyLabelsArray []*comatproto.LabelDefs_Label = []*comatproto.LabelDefs_Label{} 196 + 197 + return &appbsky.FeedDefs_PostView{ 198 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#postView"` 199 + LexiconTypeID: "app.bsky.feed.defs#postView", 200 + // Author *ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"` 201 + Author: profile, 202 + // BookmarkCount *int64 `json:"bookmarkCount,omitempty" cborgen:"bookmarkCount,omitempty"` 203 + // Cid string `json:"cid" cborgen:"cid"` 204 + Cid: *postRecordResponse.Cid, 205 + // // debug: Debug information for internal development 206 + // Debug *interface{} `json:"debug,omitempty" cborgen:"debug,omitempty"` 207 + // Embed *FeedDefs_PostView_Embed `json:"embed,omitempty" cborgen:"embed,omitempty"` 208 + Embed: postView_Embed, 209 + // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 210 + IndexedAt: postRecord.CreatedAt, 211 + // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 212 + Labels: emptyLabelsArray, 213 + // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 214 + LikeCount: &likeCount, 215 + // QuoteCount *int64 `json:"quoteCount,omitempty" cborgen:"quoteCount,omitempty"` 216 + QuoteCount: &quoteCount, 217 + // Record *lexutil.LexiconTypeDecoder `json:"record" cborgen:"record"` 218 + Record: lexicontypedecoder, 219 + // ReplyCount *int64 `json:"replyCount,omitempty" cborgen:"replyCount,omitempty"` 220 + ReplyCount: &replyCount, 221 + // RepostCount *int64 `json:"repostCount,omitempty" cborgen:"repostCount,omitempty"` 222 + RepostCount: &repostCount, 223 + // Threadgate *FeedDefs_ThreadgateView `json:"threadgate,omitempty" cborgen:"threadgate,omitempty"` 224 + // Uri string `json:"uri" cborgen:"uri"` 225 + Uri: postRecordResponse.Uri, 226 + // Viewer *FeedDefs_ViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 227 + Viewer: viewerState, 228 + }, &postRecord, nil 229 + }
+537
shims/lex/app/bsky/unspecced/getpostthreadv2/query.go
··· 1 + package appbskyunspeccedgetpostthreadv2 2 + 3 + import ( 4 + "net/http" 5 + "strconv" 6 + "sync" 7 + 8 + "context" 9 + "fmt" 10 + "log" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "github.com/gin-gonic/gin" 14 + "tangled.org/whey.party/red-dwarf-server/microcosm" 15 + "tangled.org/whey.party/red-dwarf-server/shims/utils" 16 + 17 + "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 18 + appbskyfeeddefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/feed/defs" 19 + 20 + appbsky "github.com/bluesky-social/indigo/api/bsky" 21 + ) 22 + 23 + func HandleGetPostThreadV2(c *gin.Context, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string) { 24 + ctx := c.Request.Context() 25 + 26 + rawdid := c.GetString("user_did") 27 + var viewer *utils.DID 28 + didval, errdid := utils.NewDID(rawdid) 29 + if errdid != nil { 30 + viewer = nil 31 + } else { 32 + viewer = &didval 33 + } 34 + 35 + threadAnchorURIraw := c.Query("anchor") 36 + if threadAnchorURIraw == "" { 37 + c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 38 + return 39 + } 40 + 41 + threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 42 + if err != nil { 43 + return 44 + } 45 + 46 + //var thread []*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem 47 + 48 + var skeletonposts []string 49 + 50 + emptystrarray := &[]string{} 51 + limit := 100 52 + 53 + // recurse to the top 54 + parentsMap := map[string]*appbsky.FeedDefs_PostView{} 55 + current := threadAnchorURI 56 + iteration := 0 57 + for true { 58 + iteration = iteration + 1 59 + if iteration > 10 { 60 + break 61 + } 62 + recordaturi, err := syntax.ParseATURI("at://" + current.Authority().String() + "/" + string(current.Collection()) + "/" + string(current.RecordKey())) 63 + if err != nil { 64 + break 65 + } 66 + // loop 67 + postView, postRecord, err := appbskyfeeddefs.PostView(ctx, recordaturi.String(), sl, cs, imgcdn, viewer, 3) 68 + if err != nil { 69 + break 70 + } 71 + if postView == nil { 72 + break 73 + } 74 + //if iteration != 1 { 75 + skeletonposts = append([]string{recordaturi.String()}, skeletonposts...) 76 + //} 77 + parentsMap[recordaturi.String()] = postView 78 + if postRecord != nil && postRecord.Reply != nil && postRecord.Reply.Parent != nil && postRecord.Reply.Root != nil { 79 + if postRecord.Reply.Parent.Uri == postRecord.Reply.Root.Uri { 80 + //break 81 + iteration = 9 82 + } 83 + 84 + current, err = syntax.ParseATURI(postRecord.Reply.Parent.Uri) 85 + if err != nil { 86 + break 87 + } 88 + } else { 89 + log.Println("what huh what hwat") 90 + break //whatt 91 + } 92 + 93 + } 94 + 95 + //skeletonposts = append(skeletonposts, threadAnchorURI.String()) 96 + // todo: theres a cursor!!! pagination please! 97 + // todo: also i doubt im gonna do proper threadding right now, so make sure to remind me to do it properly some time later thanks 98 + //rootReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, nil) 99 + parentReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, nil) 100 + 101 + for _, rec := range parentReplies.Records { 102 + recordaturi, err := syntax.ParseATURI("at://" + rec.Did + "/" + rec.Collection + "/" + rec.Rkey) 103 + if err != nil { 104 + continue 105 + } 106 + skeletonposts = append(skeletonposts, recordaturi.String()) 107 + } 108 + maplen := len(parentsMap) 109 + concurrentResults := MapConcurrent( 110 + ctx, 111 + skeletonposts, 112 + 20, 113 + func(ctx context.Context, raw string, idx int) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) { 114 + var postView *appbsky.FeedDefs_PostView 115 + fromParentChain := false 116 + postView, ok := parentsMap[raw] 117 + if !ok { 118 + post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, imgcdn, viewer, 3) 119 + if err != nil { 120 + return nil, err 121 + } 122 + if post == nil { 123 + return nil, fmt.Errorf("post not found") 124 + } 125 + postView = post 126 + } else { 127 + fromParentChain = true 128 + } 129 + 130 + depth := int64(1) 131 + if raw == threadAnchorURI.String() { 132 + depth = 0 133 + } 134 + if fromParentChain { 135 + depth = int64(0 - maplen + idx + 1) 136 + } 137 + 138 + return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{ 139 + // Depth int64 `json:"depth" cborgen:"depth"` 140 + Depth: depth, // todo: placeholder 141 + // Uri string `json:"uri" cborgen:"uri"` 142 + Uri: raw, 143 + // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"` 144 + Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{ 145 + // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost 146 + UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{ 147 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"` 148 + LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost", 149 + // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. 150 + // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"` 151 + HiddenByThreadgate: false, // todo: placeholder 152 + // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents. 153 + // MoreParents bool `json:"moreParents" cborgen:"moreParents"` 154 + MoreParents: false, // todo: placeholder 155 + // // moreReplies: This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate. 156 + // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"` 157 + MoreReplies: 0, // todo: placeholder 158 + // // mutedByViewer: This is by an account muted by the viewer requesting it. 159 + // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"` 160 + MutedByViewer: false, // todo: placeholder 161 + // // opThread: This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread. 162 + // OpThread bool `json:"opThread" cborgen:"opThread"` 163 + OpThread: false, // todo: placeholder 164 + // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 165 + Post: postView, 166 + }, 167 + }, 168 + }, nil 169 + }, 170 + ) 171 + 172 + // build final slice 173 + out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults)) 174 + for _, r := range concurrentResults { 175 + if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil { 176 + out = append(out, r.Value) 177 + } 178 + } 179 + 180 + // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{ 181 + // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"` 182 + // Thread: out, 183 + // HasOtherReplies: false, 184 + // }) 185 + resp := &GetPostThreadOtherV2_Output_WithOtherReplies{ 186 + UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{ 187 + Thread: out, 188 + }, 189 + HasOtherReplies: false, 190 + } 191 + c.JSON(http.StatusOK, resp) 192 + } 193 + 194 + type GetPostThreadOtherV2_Output_WithOtherReplies struct { 195 + appbsky.UnspeccedGetPostThreadOtherV2_Output 196 + HasOtherReplies bool `json:"hasOtherReplies"` 197 + } 198 + 199 + type AsyncResult[T any] struct { 200 + Value T 201 + Err error 202 + } 203 + 204 + func MapConcurrent[T any, R any]( 205 + ctx context.Context, 206 + items []T, 207 + concurrencyLimit int, 208 + mapper func(context.Context, T, int) (R, error), 209 + ) []AsyncResult[R] { 210 + if len(items) == 0 { 211 + return nil 212 + } 213 + 214 + results := make([]AsyncResult[R], len(items)) 215 + var wg sync.WaitGroup 216 + 217 + sem := make(chan struct{}, concurrencyLimit) 218 + 219 + for i, item := range items { 220 + wg.Add(1) 221 + go func(idx int, input T) { 222 + defer wg.Done() 223 + 224 + sem <- struct{}{} 225 + defer func() { <-sem }() 226 + 227 + if ctx.Err() != nil { 228 + results[idx] = AsyncResult[R]{Err: ctx.Err()} 229 + return 230 + } 231 + 232 + val, err := mapper(ctx, input, idx) 233 + results[idx] = AsyncResult[R]{Value: val, Err: err} 234 + }(i, item) 235 + } 236 + 237 + wg.Wait() 238 + return results 239 + } 240 + 241 + type SkeletonPost struct { 242 + post syntax.ATURI 243 + depth int 244 + } 245 + 246 + func HandleGetPostThreadV2V3(c *gin.Context, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string, existingGraph *ThreadGraph) *ThreadGraph { 247 + ctx := c.Request.Context() 248 + 249 + rawdid := c.GetString("user_did") 250 + var viewer *utils.DID 251 + didval, errdid := utils.NewDID(rawdid) 252 + if errdid != nil { 253 + viewer = nil 254 + } else { 255 + viewer = &didval 256 + } 257 + 258 + threadAnchorURIraw := c.Query("anchor") 259 + if threadAnchorURIraw == "" { 260 + c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 261 + return existingGraph 262 + } 263 + 264 + // "Whether to include parents above the anchor. 265 + // bool as string 266 + // true as default 267 + aboveParam := c.Query("above") // why would you need above = false ? 268 + above := true 269 + if aboveParam == "false" { 270 + above = false 271 + } 272 + 273 + // "How many levels of replies to include below the anchor." 274 + // integer as string 275 + // default: 6, min: 0, max: 20 276 + belowParam := c.Query("below") // bskydefault: 10 277 + below, err_below := strconv.ParseInt(belowParam, 10, 64) 278 + if err_below != nil { 279 + below = 6 280 + } else { 281 + if below > 20 { 282 + below = 20 283 + } 284 + if below < 0 { 285 + below = 0 286 + } 287 + } 288 + 289 + // "Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, 290 + // which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated)." 291 + // integer as string 292 + // default: 10, min: 0, max: 100 293 + branchingFactorParam := c.Query("branchingFactor") // bskydefault: 1 294 + branchingFactor, err_branchingFactor := strconv.ParseInt(branchingFactorParam, 10, 64) 295 + if err_branchingFactor != nil { 296 + branchingFactor = 10 297 + } else { 298 + if branchingFactor > 100 { 299 + branchingFactor = 100 300 + } 301 + if branchingFactor < 0 { 302 + branchingFactor = 0 303 + } 304 + } 305 + 306 + // "Sorting for the thread replies" 307 + // string (enum) ["newest", "oldest", "top"] 308 + // default: "oldest" 309 + sortParam := c.Query("sort") // bskydefault: top 310 + sort := sortParam 311 + if sort != "newest" && sort != "oldest" && sort != "top" { 312 + sort = "top" 313 + } 314 + 315 + threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 316 + if err != nil { 317 + return existingGraph 318 + } 319 + 320 + var workingGraph *ThreadGraph 321 + 322 + if existingGraph != nil { 323 + workingGraph = existingGraph 324 + // update the existing graph to fit our needs in our subtree 325 + workingGraph.UpdateGraphTo(threadAnchorURI) 326 + } else { 327 + newGraph, err := ThreadGrapher(ctx, cs, sl, threadAnchorURI) 328 + if err != nil { 329 + c.JSON(http.StatusBadRequest, gin.H{"error": ("failed to graph the thread: " + err.Error())}) 330 + return nil 331 + } 332 + workingGraph = newGraph 333 + } 334 + 335 + var skeletonposts []SkeletonPost 336 + 337 + // Parent Chain 338 + parentChainHeight := 0 339 + 340 + // root := threadGraph.RootURI 341 + if above { 342 + current := threadAnchorURI 343 + 344 + for true { 345 + log.Println("[parent threader] current: " + current.Authority().String() + string(current.Collection()) + string(current.RecordKey())) 346 + if parentChainHeight > 20 { 347 + // root = current 348 + break 349 + } 350 + parent, ok := workingGraph.ParentsMap[current] 351 + if !ok { 352 + // root = current 353 + break 354 + } 355 + parentChainHeight = parentChainHeight + 1 356 + skeletonposts = append(skeletonposts, SkeletonPost{ 357 + post: parent, 358 + depth: -parentChainHeight, 359 + }) 360 + current = parent 361 + } 362 + flipArray(&skeletonposts) 363 + } 364 + 365 + // handled by the recurser 366 + // // Anchor Post 367 + // skeletonposts = append(skeletonposts, SkeletonPost{ 368 + // post: threadAnchorURI, 369 + // depth: 0, 370 + // }) 371 + 372 + // Tree Replies (with OP thread priority) 373 + // should probably be recursive 374 + recursiveHandleV2V3TreeReplies(workingGraph, &skeletonposts, threadAnchorURI, &below, &branchingFactor, &sort, 0, 0) 375 + 376 + //maplen := len(parentsMap) 377 + concurrentResults := MapConcurrent( 378 + ctx, 379 + skeletonposts, 380 + 20, 381 + func(ctx context.Context, raw SkeletonPost, idx int) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) { 382 + var postView *appbsky.FeedDefs_PostView 383 + //fromParentChain := false 384 + //postView, ok := parentsMap[raw] 385 + //if !ok { 386 + post, _, err := appbskyfeeddefs.PostView(ctx, raw.post.String(), sl, cs, imgcdn, viewer, 3) 387 + if err != nil { 388 + return nil, err 389 + } 390 + if post == nil { 391 + return nil, fmt.Errorf("post not found") 392 + } 393 + postView = post 394 + //} else { 395 + // fromParentChain = true 396 + //} 397 + 398 + depth := int64(1) 399 + // if raw == threadAnchorURI.String() { 400 + // depth = 0 401 + // } 402 + // if fromParentChain { 403 + // depth = int64(0 - parentChainHeight + idx + 1) 404 + // } 405 + depth = int64(raw.depth) 406 + 407 + return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{ 408 + // Depth int64 `json:"depth" cborgen:"depth"` 409 + Depth: depth, // todo: placeholder 410 + // Uri string `json:"uri" cborgen:"uri"` 411 + Uri: raw.post.String(), 412 + // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"` 413 + Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{ 414 + // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost 415 + UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{ 416 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"` 417 + LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost", 418 + // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. 419 + // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"` 420 + HiddenByThreadgate: false, // todo: placeholder 421 + // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents. 422 + // MoreParents bool `json:"moreParents" cborgen:"moreParents"` 423 + MoreParents: false, // todo: placeholder 424 + // // moreReplies: This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate. 425 + // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"` 426 + MoreReplies: 0, // todo: placeholder 427 + // // mutedByViewer: This is by an account muted by the viewer requesting it. 428 + // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"` 429 + MutedByViewer: false, // todo: placeholder 430 + // // opThread: This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread. 431 + // OpThread bool `json:"opThread" cborgen:"opThread"` 432 + OpThread: false, // todo: placeholder 433 + // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 434 + Post: postView, 435 + }, 436 + }, 437 + }, nil 438 + }, 439 + ) 440 + 441 + // build final slice 442 + out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults)) 443 + for _, r := range concurrentResults { 444 + if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil { 445 + out = append(out, r.Value) 446 + } 447 + } 448 + 449 + // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{ 450 + // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"` 451 + // Thread: out, 452 + // HasOtherReplies: false, 453 + // }) 454 + resp := &GetPostThreadOtherV2_Output_WithOtherReplies{ 455 + UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{ 456 + Thread: out, 457 + }, 458 + HasOtherReplies: false, 459 + } 460 + c.JSON(http.StatusOK, resp) 461 + 462 + return workingGraph 463 + } 464 + 465 + func flipArray[T any](s *[]T) { 466 + for i, j := 0, len(*s)-1; i < j; i, j = i+1, j-1 { 467 + (*s)[i], (*s)[j] = (*s)[j], (*s)[i] 468 + } 469 + } 470 + 471 + func recursiveHandleV2V3TreeReplies(threadGraph *ThreadGraph, skeletonposts *[]SkeletonPost, current syntax.ATURI, below *int64, branchingFactor *int64, sort *string, verticalPos int64, horizontalPos int64) { 472 + log.Println("[V3 Recurse] at y: " + fmt.Sprint(verticalPos) + ", x: " + fmt.Sprint(horizontalPos)) 473 + // breakings 474 + if below != nil && verticalPos > *below { 475 + log.Println("[V3 Recurse] exit by too low") 476 + return 477 + } 478 + if branchingFactor != nil && horizontalPos > *branchingFactor && verticalPos > 1 { 479 + log.Println("[V3 Recurse] exit by too wide; branchingFactor: " + fmt.Sprint(*branchingFactor) + "; horizontalPos: " + fmt.Sprint(horizontalPos) + "; verticalPos: " + fmt.Sprint(verticalPos)) 480 + return 481 + } 482 + 483 + // the things to do if not recurse 484 + if skeletonposts != nil { 485 + *skeletonposts = append(*skeletonposts, SkeletonPost{ 486 + post: current, 487 + depth: int(verticalPos), 488 + }) 489 + } else { 490 + log.Println("[V3 Recurse] exit by no skeleton posts") 491 + return 492 + } 493 + 494 + repliesAtThisPosition, ok := threadGraph.ChildrenMap[current] 495 + if !ok { 496 + log.Println("[V3 Recurse] exit by no replies") 497 + return 498 + } 499 + 500 + // recurse 501 + op := threadGraph.RootURI.Authority() 502 + 503 + // We need a modifyable copy of the slice because we are about to reorder it 504 + // and we don't want to mess up the original map in case it's used elsewhere. 505 + sortedReplies := make([]syntax.ATURI, len(repliesAtThisPosition)) 506 + copy(sortedReplies, repliesAtThisPosition) 507 + 508 + // 1. Find the "best" OP reply (lexicographically smallest rkey usually means oldest) 509 + bestOpIndex := -1 510 + var bestOpURI syntax.ATURI 511 + 512 + for i, replyURI := range sortedReplies { 513 + if replyURI.Authority() == op { 514 + // If this is the first OP reply we found, or if it's lexicographically smaller (older) than the previous best 515 + if bestOpIndex == -1 || replyURI.String() < bestOpURI.String() { 516 + bestOpIndex = i 517 + bestOpURI = replyURI 518 + } 519 + } 520 + } 521 + 522 + // 2. If we found an OP reply, move it to index 0 523 + if bestOpIndex > 0 { // If it's already 0, no need to move 524 + // Remove the best OP reply from its current spot 525 + // (Go slice trick: append everything before it + everything after it) 526 + withoutBest := append(sortedReplies[:bestOpIndex], sortedReplies[bestOpIndex+1:]...) 527 + 528 + // Prepend it to the front 529 + sortedReplies = append([]syntax.ATURI{bestOpURI}, withoutBest...) 530 + } 531 + 532 + for idx, reply := range sortedReplies { 533 + recursiveHandleV2V3TreeReplies(threadGraph, skeletonposts, reply, below, branchingFactor, sort, verticalPos+1, int64(idx+1)) 534 + } 535 + 536 + // going up 537 + }
+250
shims/lex/app/bsky/unspecced/getpostthreadv2/threadgrapher.go
··· 1 + package appbskyunspeccedgetpostthreadv2 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "log" 8 + "sync" 9 + 10 + "github.com/bluesky-social/indigo/api/agnostic" 11 + //comatproto "github.com/bluesky-social/indigo/api/atproto" 12 + appbsky "github.com/bluesky-social/indigo/api/bsky" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + 15 + "tangled.org/whey.party/red-dwarf-server/microcosm" 16 + "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 17 + ) 18 + 19 + type ThreadGraph struct { 20 + mu sync.RWMutex // Mutex required for thread-safe updates (Firehose) 21 + 22 + RootURI syntax.ATURI 23 + AnchorURI syntax.ATURI 24 + 25 + ParentsMap map[syntax.ATURI]syntax.ATURI 26 + 27 + ChildrenMap map[syntax.ATURI][]syntax.ATURI 28 + } 29 + 30 + func NewThreadGraph() *ThreadGraph { 31 + return &ThreadGraph{ 32 + ParentsMap: make(map[syntax.ATURI]syntax.ATURI), 33 + ChildrenMap: make(map[syntax.ATURI][]syntax.ATURI), 34 + } 35 + } 36 + 37 + func ThreadGrapher(ctx context.Context, cs *microcosm.MicrocosmClient, sl *microcosm.MicrocosmClient, threadAnchorURI syntax.ATURI) (*ThreadGraph, error) { 38 + tg := NewThreadGraph() 39 + tg.AnchorURI = threadAnchorURI 40 + 41 + anchorPostRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.post", threadAnchorURI.Authority().String(), string(threadAnchorURI.RecordKey())) 42 + if err != nil { 43 + log.Println("[ThreadGrapher] exit by anchor post resolve failure") 44 + return nil, fmt.Errorf("failed to fetch anchor: %w", err) 45 + } 46 + 47 + var anchorPost appbsky.FeedPost 48 + if err := json.Unmarshal(*anchorPostRecordResponse.Value, &anchorPost); err != nil { 49 + log.Println("[ThreadGrapher] exit by no json") 50 + return nil, fmt.Errorf("error: Failed to parse post record JSON") 51 + } 52 + 53 + var rootURI syntax.ATURI 54 + 55 + if anchorPost.Reply != nil && anchorPost.Reply.Root != nil { 56 + rURI, err := syntax.ParseATURI(anchorPost.Reply.Root.Uri) 57 + if err != nil { 58 + log.Println("[ThreadGrapher] exit by invalid root uri") 59 + return nil, fmt.Errorf("invalid root uri in record: %w", err) 60 + } 61 + rootURI = rURI 62 + 63 + // todo: fiine we wont fetch the root post, but we still need to mark it as deleted somehow 64 + // rootPostRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.post", rootURI.Authority().String(), string(rootURI.RecordKey())) 65 + // if err != nil { 66 + // log.Println("[ThreadGrapher] exit by cant fetch root") 67 + // return nil, fmt.Errorf("failed to fetch root: %w", err) 68 + // } 69 + 70 + // var rootPost appbsky.FeedPost 71 + // if err := json.Unmarshal(*rootPostRecordResponse.Value, &rootPost); err != nil { 72 + // log.Println("[ThreadGrapher] exit by cant parse root json") 73 + // return nil, fmt.Errorf("error: Failed to parse post record JSON") 74 + // } 75 + // if err == nil { 76 + // tg.AddNode(rootURI, &rootPost) 77 + // } 78 + } else { 79 + rootURI = threadAnchorURI 80 + } 81 + tg.RootURI = rootURI 82 + 83 + emptystrarray := &[]string{} 84 + var allRepliesATURI []syntax.ATURI 85 + limit := 100 86 + var cursor *string 87 + shouldContinue := true 88 + for shouldContinue { 89 + results, err := constellation.GetBacklinks(ctx, cs, rootURI.String(), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, cursor) 90 + if err != nil { 91 + log.Println("[ThreadGrapher] [root graphing] exit by backlink failure") 92 + return nil, fmt.Errorf("failed to get backlinks: %w", err) 93 + } 94 + if results.Records != nil { 95 + for _, record := range results.Records { 96 + aturi, err := syntax.ParseATURI("at://" + record.Did + "/" + record.Collection + "/" + record.Rkey) 97 + if err == nil { 98 + allRepliesATURI = append(allRepliesATURI, aturi) 99 + } 100 + } 101 + } 102 + if results.Cursor != nil { 103 + cursor = results.Cursor 104 + } else { 105 + shouldContinue = false 106 + } 107 + } 108 + 109 + processingQueue := append([]syntax.ATURI{rootURI}, allRepliesATURI...) 110 + // for _, aturi := range processingQueue { 111 + // //tg.Nodes = append(tg.Nodes, aturi) 112 + // // graphinger 113 + // emptystrarray := &[]string{} 114 + // var localRepliesATURI []syntax.ATURI 115 + // limit := 100 116 + // var cursor *string 117 + // shouldContinue := true 118 + // for shouldContinue { 119 + // results, err := constellation.GetBacklinks(ctx, cs, aturi.String(), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, cursor) 120 + // if err != nil { 121 + // log.Println("[ThreadGrapher] [parent graphing] exit by no replies") 122 + // return nil, fmt.Errorf("failed to get backlinks: %w", err) 123 + // } 124 + // if results.Records != nil { 125 + // for _, record := range results.Records { 126 + // aturi, err := syntax.ParseATURI("at://" + record.Did + "/" + record.Collection + "/" + record.Rkey) 127 + // if err == nil { 128 + // localRepliesATURI = append(localRepliesATURI, aturi) 129 + // } 130 + // } 131 + // } 132 + // if results.Cursor != nil { 133 + // cursor = results.Cursor 134 + // } else { 135 + // shouldContinue = false 136 + // } 137 + // } 138 + // for _, reply := range localRepliesATURI { 139 + // tg.ParentsMap[reply] = aturi 140 + // tg.ChildrenMap[aturi] = append(tg.ChildrenMap[aturi], reply) 141 + // } 142 + // } 143 + 144 + type concurrentStruct struct { 145 + aturi syntax.ATURI 146 + replies []syntax.ATURI 147 + } 148 + 149 + localRepliesATURIConcurrent := MapConcurrent( 150 + ctx, 151 + processingQueue, 152 + 50, 153 + func(ctx context.Context, aturi syntax.ATURI, idx int) (*concurrentStruct, error) { 154 + //tg.Nodes = append(tg.Nodes, aturi) 155 + // graphinger 156 + emptystrarray := &[]string{} 157 + var localRepliesATURI []syntax.ATURI 158 + limit := 100 159 + var cursor *string 160 + shouldContinue := true 161 + for shouldContinue { 162 + results, err := constellation.GetBacklinks(ctx, cs, aturi.String(), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, cursor) 163 + if err != nil { 164 + log.Println("[ThreadGrapher] [parent graphing] exit by no replies") 165 + return nil, fmt.Errorf("failed to get backlinks: %w", err) 166 + } 167 + if results.Records != nil { 168 + for _, record := range results.Records { 169 + aturi, err := syntax.ParseATURI("at://" + record.Did + "/" + record.Collection + "/" + record.Rkey) 170 + if err == nil { 171 + localRepliesATURI = append(localRepliesATURI, aturi) 172 + } 173 + } 174 + } 175 + if results.Cursor != nil { 176 + cursor = results.Cursor 177 + } else { 178 + shouldContinue = false 179 + } 180 + } 181 + return &concurrentStruct{ 182 + aturi: aturi, 183 + replies: localRepliesATURI, 184 + }, nil 185 + }, 186 + ) 187 + 188 + localRepliesATURI := make([]*concurrentStruct, 0, len(localRepliesATURIConcurrent)) 189 + for _, r := range localRepliesATURIConcurrent { 190 + if /*r != nil &&*/ r.Err == nil && r.Value != nil /*&& r.Value.aturi != nil*/ && r.Value.replies != nil { 191 + localRepliesATURI = append(localRepliesATURI, r.Value) 192 + } 193 + } 194 + for _, replyStruct := range localRepliesATURI { 195 + aturi := replyStruct.aturi 196 + for _, reply := range replyStruct.replies { 197 + tg.ParentsMap[reply] = aturi 198 + tg.ChildrenMap[aturi] = append(tg.ChildrenMap[aturi], reply) 199 + } 200 + } 201 + 202 + return tg, nil 203 + } 204 + 205 + // ToBytes serializes the ThreadGraph into a JSON byte slice. 206 + // It acquires a Read Lock to ensure thread safety during serialization. 207 + func (g *ThreadGraph) ToBytes() ([]byte, error) { 208 + g.mu.RLock() 209 + defer g.mu.RUnlock() 210 + 211 + // sync.RWMutex is unexported, so json.Marshal will automatically skip it. 212 + // syntax.ATURI implements TextMarshaler, so it works as a map key automatically. 213 + return json.Marshal(g) 214 + } 215 + 216 + // ThreadGraphFromBytes deserializes a byte slice back into a ThreadGraph. 217 + // It ensures maps are initialized even if the JSON data was empty. 218 + func ThreadGraphFromBytes(data []byte) (*ThreadGraph, error) { 219 + // Initialize with NewThreadGraph to ensure maps are allocated 220 + tg := NewThreadGraph() 221 + 222 + if err := json.Unmarshal(data, tg); err != nil { 223 + return nil, fmt.Errorf("failed to deserialize ThreadGraph: %w", err) 224 + } 225 + 226 + // Safety check: specific to Go's JSON unmarshal behavior. 227 + // If the JSON contained "null" for the maps, they might be nil again. 228 + // We re-initialize them to avoid panics during concurrent writes later. 229 + if tg.ParentsMap == nil { 230 + tg.ParentsMap = make(map[syntax.ATURI]syntax.ATURI) 231 + } 232 + if tg.ChildrenMap == nil { 233 + tg.ChildrenMap = make(map[syntax.ATURI][]syntax.ATURI) 234 + } 235 + 236 + // The Mutex (tg.mu) is zero-valued (unlocked) by default, which is exactly what we want. 237 + return tg, nil 238 + } 239 + 240 + func (g *ThreadGraph) UpdateGraphTo(anchor syntax.ATURI) { 241 + // path from anchor to root never needs to be updated 242 + // all we need is to update all subtrees of the anchor 243 + // so we should first do a 244 + // recursiveHandleUpdateGraphTo(g, anchor) 245 + // i dont think we should do a recursive thing 246 + // it cant be optimized well, constellation queries will be sequential 247 + // how about, grab the entire tree again, prune branches not part of the tree 248 + // you will get a list of posts that are either new or a part of the subtree 249 + // 250 + }
+46
shims/utils/utils.go
··· 1 + package utils 2 + 3 + import ( 4 + "fmt" 5 + "regexp" 6 + ) 7 + 8 + type DID string 9 + 10 + var didPattern = regexp.MustCompile(`^did:(plc|web):.+$`) 11 + 12 + func NewDID(s string) (DID, error) { 13 + if !didPattern.MatchString(s) { 14 + return "", fmt.Errorf("invalid DID: %s", s) 15 + } 16 + return DID(s), nil 17 + } 18 + 19 + type AtURI string 20 + 21 + var atUriPattern = regexp.MustCompile(`^at://did:(plc|web):.+/.+/.+$`) 22 + 23 + func NewAtURI(s string) (AtURI, error) { 24 + if !atUriPattern.MatchString(s) { 25 + return "", fmt.Errorf("invalid AtURI: %s", s) 26 + } 27 + return AtURI(s), nil 28 + } 29 + 30 + func SafeStringPtr(s *string) *string { 31 + if s != nil { 32 + return s 33 + } 34 + return nil 35 + } 36 + 37 + func PtrString(s string) *string { return &s } 38 + 39 + func MakeImageCDN(did DID, imgcdn string, kind string, cid string) string { 40 + return imgcdn + "/img/" + kind + "/plain/" + string(did) + "/" + cid + "@jpeg" 41 + } 42 + 43 + func MakeVideoCDN(did DID, videocdn string, kind string, cid string) string { 44 + //{videocdn}/watch/{uri encoded did}/{video cid}/thumbnail.jpg 45 + return videocdn + "/watch/" + string(did) + "/" + cid + "/" + kind 46 + }
+2 -2
sticket/sticket.go
··· 31 31 } 32 32 } 33 33 34 - func (m *Manager) HandleWS(w *http.ResponseWriter, r *http.Request) { 35 - conn, err := m.upgrader.Upgrade(*w, r, nil) 34 + func (m *Manager) HandleWS(w http.ResponseWriter, r *http.Request) { 35 + conn, err := m.upgrader.Upgrade(w, r, nil) 36 36 if err != nil { 37 37 log.Printf("Sticket: Failed to upgrade: %v", err) 38 38 return
+27
store/kv.go
··· 1 + package store 2 + 3 + import "sync" 4 + 5 + type KV struct { 6 + mu sync.RWMutex 7 + m map[string][]byte 8 + } 9 + 10 + func NewKV() *KV { 11 + return &KV{ 12 + m: make(map[string][]byte), 13 + } 14 + } 15 + 16 + func (kv *KV) Get(key string) ([]byte, bool) { 17 + kv.mu.RLock() 18 + defer kv.mu.RUnlock() 19 + v, ok := kv.m[key] 20 + return v, ok 21 + } 22 + 23 + func (kv *KV) Set(key string, val []byte) { 24 + kv.mu.Lock() 25 + defer kv.mu.Unlock() 26 + kv.m[key] = val 27 + }