collection of golang services under the Red Dwarf umbrella server.reddwarf.app
bluesky reddwarf microcosm appview

Compare changes

Choose any two refs to compare.

+7670 -141
+3
.gitignore
··· 1 + cmd/aturilist/badger_data 2 + cmd/backstream/temp 3 + cmd/labelmerge/badger_cache
+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 + }
+112
cmd/labelmerge/handlers.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "strconv" 7 + "time" 8 + 9 + comatprototypes "github.com/bluesky-social/indigo/api/atproto" 10 + appreddwarflabelmerge "tangled.org/whey.party/red-dwarf-server/labelmerge/lex" 11 + ) 12 + 13 + func (s *Server) handleAppReddwarfLabelmergeQueryLabels( 14 + ctx context.Context, 15 + inputl []string, 16 + inputs []string, 17 + strict *bool, 18 + ) (*appreddwarflabelmerge.QueryLabels_Output, error) { 19 + 20 + // Build the query struct your service expects 21 + query := IncomingQuery{ 22 + Labelers: inputl, 23 + Subjects: inputs, 24 + Strict: strict != nil && *strict, 25 + } 26 + 27 + // enforce limits 28 + if len(query.Labelers) > MaxLabelersPerQuery || len(query.Subjects) > MaxSubjectsPerQuery { 29 + return &appreddwarflabelmerge.QueryLabels_Output{ 30 + Error: []*appreddwarflabelmerge.QueryLabels_Error{ 31 + { // todo dont do this we should just throw error and not use the partial errors system 32 + S: "too_many_labels_or_subjects", 33 + E: ptrString(fmt.Sprintf("Labelers: %d, Subjects: %d", len(query.Labelers), len(query.Subjects))), 34 + }, 35 + }, 36 + }, nil 37 + } 38 + 39 + // Setup context with timeout 40 + ctx, cancel := context.WithTimeout(ctx, IncomingQueryTimeout) 41 + defer cancel() 42 + 43 + promise := &QueryPromise{ 44 + ID: strconv.FormatInt(time.Now().UnixNano(), 10), 45 + Query: query, 46 + Response: make(chan FinalResponse, 1), 47 + Ctx: ctx, 48 + } 49 + 50 + s.Service.HandlePromise(promise) 51 + 52 + select { 53 + case result := <-promise.Response: 54 + // convert FinalResponse -> QueryLabels_Output 55 + // Flatten your map into a slice 56 + flatLabels := make([]*comatprototypes.LabelDefs_Label, 0) 57 + for _, submap := range result.Labels { // result.Labels is map[labelerDID]map[subjectURI][]*Label 58 + for _, labels := range submap { 59 + flatLabels = append(flatLabels, SliceToPtrSlice(labels)...) 60 + } 61 + } 62 + 63 + out := &appreddwarflabelmerge.QueryLabels_Output{ 64 + Labels: flatLabels, 65 + } 66 + 67 + if result.Error != nil { 68 + for _, did := range result.Error.LabelerResolutionFailure { 69 + out.Error = append(out.Error, &appreddwarflabelmerge.QueryLabels_Error{ 70 + S: did, 71 + E: ptrString("labeler_resolution_failure"), 72 + }) 73 + } 74 + for _, did := range result.Error.QueryLabelsTooManyPages { 75 + out.Error = append(out.Error, &appreddwarflabelmerge.QueryLabels_Error{ 76 + S: did, 77 + E: ptrString("too_many_pages"), 78 + }) 79 + } 80 + for _, did := range result.Error.LabelerQueryFailure { 81 + out.Error = append(out.Error, &appreddwarflabelmerge.QueryLabels_Error{ 82 + S: did, 83 + E: ptrString("labeler_query_failure"), 84 + }) 85 + } 86 + } 87 + 88 + // handle strict mode 89 + if query.Strict && out.Error != nil && len(out.Error) > 0 { 90 + return out, fmt.Errorf("strict mode failure: %v", out.Error) 91 + } 92 + 93 + return out, nil 94 + 95 + case <-ctx.Done(): 96 + timeoutMsg := "one of the queries didn't return in time, try again later" 97 + out := &appreddwarflabelmerge.QueryLabels_Output{ 98 + Error: []*appreddwarflabelmerge.QueryLabels_Error{ 99 + { 100 + S: "timeout", // todo dont do this we should explicitly point out the slow labeler and return the succesful labels 101 + E: &timeoutMsg, 102 + }, 103 + }, 104 + } 105 + if query.Strict { 106 + return out, fmt.Errorf(timeoutMsg) 107 + } 108 + return out, nil 109 + } 110 + } 111 + 112 + func ptrString(s string) *string { return &s }
+991
cmd/labelmerge/main.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "fmt" 8 + "log" 9 + "net/http" 10 + "net/url" 11 + "slices" 12 + "strconv" 13 + "strings" 14 + "sync" 15 + "time" 16 + 17 + comatprototypes "github.com/bluesky-social/indigo/api/atproto" 18 + "github.com/bluesky-social/indigo/atproto/identity" 19 + "github.com/bluesky-social/indigo/atproto/syntax" 20 + "github.com/gin-gonic/gin" 21 + "github.com/labstack/echo/v4" 22 + "github.com/labstack/echo/v4/middleware" 23 + lru "tangled.org/whey.party/red-dwarf-server/labelmerge/lru" 24 + ) 25 + 26 + // --- Constants --- 27 + const ( 28 + ServerPort = ":3879" 29 + MaxLabelersPerQuery = 20 30 + MaxSubjectsPerQuery = 100 31 + LRUTTL = 30 * time.Minute 32 + LabelerResolverTTL = 1 * time.Hour 33 + LabelerResolutionTimeout = 4 * time.Second 34 + BusQueueBatchInterval = 500 * time.Millisecond // T seconds, set to 0.5s 35 + MaxSubjectsPerExternalRequest = 50 36 + MaxQueryPages = 16 37 + IncomingQueryTimeout = 30 * time.Second 38 + ) 39 + 40 + // --- Core Data Structures (AT Protocol & Internal) --- 41 + 42 + // Label definition updated to include CID, Neg, and Exp fields, matching the full AT Protocol specification. 43 + // type Label struct { 44 + // Ver int `json:"ver"` 45 + // Src string `json:"src"` // DID of the actor who created this label. 46 + // URI string `json:"uri"` // AT URI of the record, repository (account), or other resource. 47 + // CID string `json:"cid,omitempty"` // Optionally, CID specifying the specific version. 48 + // Val string `json:"val"` // The short string name of the value or type of this label. 49 + // Neg bool `json:"neg,omitempty"` // If true, this is a negation label. 50 + // CTS string `json:"cts"` // Timestamp when this label was created. 51 + // Exp string `json:"exp,omitempty"` // Timestamp at which this label expires. 52 + // Sig struct { 53 + // Bytes string `json:"$bytes"` 54 + // } `json:"sig"` // Signature of dag-cbor encoded label. 55 + // } 56 + 57 + type Label = comatprototypes.LabelDefs_Label 58 + 59 + /* 60 + type LabelDefs_Label struct { 61 + // cid: Optionally, CID specifying the specific version of 'uri' resource this label applies to. 62 + Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 63 + // cts: Timestamp when this label was created. 64 + Cts string `json:"cts" cborgen:"cts"` 65 + // exp: Timestamp at which this label expires (no longer applies). 66 + Exp *string `json:"exp,omitempty" cborgen:"exp,omitempty"` 67 + // neg: If true, this is a negation label, overwriting a previous label. 68 + Neg *bool `json:"neg,omitempty" cborgen:"neg,omitempty"` 69 + // sig: Signature of dag-cbor encoded label. 70 + Sig lexutil.LexBytes `json:"sig,omitempty" cborgen:"sig,omitempty"` 71 + // src: DID of the actor who created this label. 72 + Src string `json:"src" cborgen:"src"` 73 + // uri: AT URI of the record, repository (account), or other resource that this label applies to. 74 + Uri string `json:"uri" cborgen:"uri"` 75 + // val: The short string name of the value or type of this label. 76 + Val string `json:"val" cborgen:"val"` 77 + // ver: The AT Protocol version of the label object. 78 + Ver *int64 `json:"ver,omitempty" cborgen:"ver,omitempty"` 79 + } 80 + LabelDefs_Label is a "label" in the com.atproto.label.defs schema. 81 + 82 + Metadata tag on an atproto resource (eg, repo or record). 83 + 84 + func (t *comatprototypes.LabelDefs_Label) MarshalCBOR(w io.Writer) error 85 + func (t *comatprototypes.LabelDefs_Label) UnmarshalCBOR(r io.Reader) (err error) 86 + */ 87 + 88 + type IncomingQuery struct { 89 + Subjects []string `json:"s"` 90 + Labelers []string `json:"l"` 91 + Strict bool `json:"strict"` 92 + } 93 + 94 + // Req is the internal unit of work sent to the Bus Queue (Subject -> Labeler[]) 95 + type Req struct { 96 + Subject string 97 + Labelers []string 98 + } 99 + 100 + // QueryError defines the structure for reporting errors in the response body. 101 + type QueryError struct { 102 + QueryLabelsTooManyPages []string `json:"queryLabelsTooManyPages,omitempty"` 103 + LabelerResolutionFailure []string `json:"labelerResolutionFailure,omitempty"` 104 + LabelerQueryFailure []string `json:"labelerQueryFailure,omitempty"` // NEW FAILURE TYPE 105 + } 106 + 107 + func (qe *QueryError) HasLabelerError(s string) bool { 108 + sets := [][]string{ 109 + //qe.QueryLabelsTooManyPages, // not a labeler error. it should be negligable maybe with the larger tolerance of 10 pages 110 + qe.LabelerResolutionFailure, 111 + qe.LabelerQueryFailure, 112 + } 113 + 114 + for _, set := range sets { 115 + if slices.Contains(set, s) { 116 + return true 117 + } 118 + } 119 + return false 120 + } 121 + 122 + // FinalResponse is the structure returned to the client. 123 + type FinalResponse struct { 124 + Labels Results `json:"labels"` // map[labelerDID][subjectURI] -> []Label 125 + Error *QueryError `json:"error,omitempty"` 126 + } 127 + 128 + type QueryPromise struct { 129 + ID string 130 + Query IncomingQuery 131 + Response chan FinalResponse 132 + Ctx context.Context 133 + Done sync.Once 134 + } 135 + 136 + // ResolvedLabels represents labels for a subject/labeler pair (empty slice means success, but no labels) 137 + type ResolvedLabels = comatprototypes.LabelSubscribeLabels_Labels //[]Label // keep seq for future use but in the mean time use 67 138 + 139 + // Labeler Domain Cache Entry 140 + type LabelerDomainEntry struct { 141 + Domain string 142 + ExpiresAt time.Time 143 + Err error // Store resolution errors temporarily 144 + } 145 + 146 + // globalPromiseTrackers maps Promise ID to the state object tracking its completion. 147 + var globalPromiseTrackers = struct { 148 + mu sync.Mutex 149 + m map[string]*PromiseTracker 150 + }{m: make(map[string]*PromiseTracker)} 151 + 152 + type Results map[string]map[string][]Label 153 + 154 + // PromiseTracker manages the state of a single incoming promise, aggregating partial results. 155 + type PromiseTracker struct { 156 + P *QueryPromise 157 + 158 + Mu sync.Mutex 159 + 160 + TotalPairs int // Total (subject, labeler) pairs requested 161 + CompletedPairs int 162 + Done chan struct{} 163 + 164 + // Aggregated Results and Errors 165 + Results Results 166 + Errors *QueryError 167 + } 168 + 169 + func (r Results) FilterErrorLabels() Results { 170 + // please document this! but empty labels objects is inferred to be an error 171 + // since earlier we make it so if error it result in an empty labels object 172 + out := make(Results, len(r)) 173 + 174 + for k, v := range r { 175 + if len(v) == 0 { 176 + continue 177 + } 178 + out[k] = v 179 + } 180 + 181 + return out 182 + } 183 + 184 + func NewPromiseTracker(p *QueryPromise, totalPairs int) *PromiseTracker { 185 + // Initialize Results map 186 + results := make(Results) 187 + for _, l := range p.Query.Labelers { 188 + results[l] = make(map[string][]Label) 189 + } 190 + 191 + return &PromiseTracker{ 192 + P: p, 193 + TotalPairs: totalPairs, 194 + CompletedPairs: 0, 195 + Done: make(chan struct{}), 196 + Results: results, 197 + Errors: &QueryError{}, 198 + } 199 + } 200 + 201 + // warning take great care when using this 202 + func SliceToPtrSlice[T any](in []T) []*T { 203 + out := make([]*T, len(in)) 204 + for i := range in { 205 + out[i] = &in[i] 206 + } 207 + return out 208 + } 209 + 210 + // warning take great care when using this 211 + func PtrSliceToSlice[T any](in []*T) []T { 212 + out := make([]T, len(in)) 213 + for i, p := range in { 214 + if p != nil { 215 + out[i] = *p 216 + } 217 + } 218 + return out 219 + } 220 + 221 + // StoreResult atomically stores a result (or error) and checks if the promise is complete. 222 + func (t *PromiseTracker) StoreResult(labelerDID, subjectURI string, labels []*Label, isExternalFetch bool) { 223 + t.Mu.Lock() 224 + defer t.Mu.Unlock() 225 + 226 + if t.CompletedPairs >= t.TotalPairs { 227 + return 228 + } 229 + 230 + // Store Labels 231 + // only if labelerDID doesnt error out 232 + if !t.Errors.HasLabelerError(labelerDID) { 233 + if t.Results[labelerDID] == nil { 234 + t.Results[labelerDID] = make(map[string][]Label) 235 + } 236 + t.Results[labelerDID][subjectURI] = PtrSliceToSlice(labels) 237 + } 238 + 239 + t.CompletedPairs++ 240 + 241 + // Check Completion 242 + if t.CompletedPairs == t.TotalPairs { 243 + close(t.Done) 244 + } 245 + } 246 + 247 + // FinalizeAndRespond constructs the final response, sends it, and cleans up the tracker. 248 + func (t *PromiseTracker) FinalizeAndRespond(p *QueryPromise, timeoutError string) { 249 + log.Println("response done (finalizing)!") 250 + p.Done.Do(func() { 251 + resp := FinalResponse{ 252 + Labels: t.Results.FilterErrorLabels(), 253 + } 254 + 255 + if timeoutError != "" { 256 + resp.Labels = nil 257 + resp.Error = &QueryError{ 258 + LabelerResolutionFailure: []string{timeoutError}, 259 + } 260 + } else if len(t.Errors.LabelerResolutionFailure) > 0 || 261 + len(t.Errors.QueryLabelsTooManyPages) > 0 || 262 + len(t.Errors.LabelerQueryFailure) > 0 { // Check the new error field 263 + resp.Error = t.Errors 264 + } 265 + 266 + // Cleanup: Remove tracker from global map 267 + globalPromiseTrackers.mu.Lock() 268 + delete(globalPromiseTrackers.m, p.ID) 269 + globalPromiseTrackers.mu.Unlock() 270 + 271 + p.Response <- resp 272 + }) 273 + } 274 + 275 + // --- Component Implementations --- 276 + 277 + // InFlightManager handles deduplication and tracks waiting promises. 278 + type InFlightManager struct { 279 + mu sync.Mutex 280 + // Key: subject|labelerDID. Value: list of promises waiting for this result. 281 + requests map[string][]*QueryPromise 282 + } 283 + 284 + func NewInFlightManager() *InFlightManager { 285 + return &InFlightManager{requests: make(map[string][]*QueryPromise)} 286 + } 287 + 288 + func (m *InFlightManager) key(subject, labeler string) string { 289 + return subject + "|" + labeler 290 + } 291 + 292 + // CheckAndRegister registers the promise to wait. Returns true if already in flight. 293 + func (m *InFlightManager) CheckAndRegister(subject, labeler string, p *QueryPromise) bool { 294 + k := m.key(subject, labeler) 295 + m.mu.Lock() 296 + defer m.mu.Unlock() 297 + 298 + if _, exists := m.requests[k]; exists { 299 + m.requests[k] = append(m.requests[k], p) 300 + return true 301 + } 302 + 303 + m.requests[k] = []*QueryPromise{p} 304 + return false 305 + } 306 + 307 + // Resolve retrieves waiting promises and clears the entry. 308 + func (m *InFlightManager) Resolve(subject, labeler string) []*QueryPromise { 309 + k := m.key(subject, labeler) 310 + m.mu.Lock() 311 + defer m.mu.Unlock() 312 + 313 + waiters, exists := m.requests[k] 314 + if !exists { 315 + return nil 316 + } 317 + 318 + delete(m.requests, k) 319 + return waiters 320 + } 321 + 322 + // RemovePromise removes a specific promise from all in-flight requests (used for cleanup) 323 + func (m *InFlightManager) RemovePromise(p *QueryPromise) { 324 + m.mu.Lock() 325 + defer m.mu.Unlock() 326 + 327 + for k, waiters := range m.requests { 328 + filtered := make([]*QueryPromise, 0, len(waiters)) 329 + for _, waiter := range waiters { 330 + if waiter.ID != p.ID { 331 + filtered = append(filtered, waiter) 332 + } 333 + } 334 + if len(filtered) == 0 { 335 + delete(m.requests, k) 336 + } else { 337 + m.requests[k] = filtered 338 + } 339 + } 340 + } 341 + 342 + // --- Main Service Coordinator --- 343 + 344 + type LabelResolutionService struct { 345 + incomingPromises chan *QueryPromise 346 + busQueueIn chan Req 347 + 348 + lru *lru.Cache[string, ResolvedLabels] 349 + labelerResolver identity.Directory 350 + inFlightManager *InFlightManager 351 + 352 + wg sync.WaitGroup 353 + } 354 + 355 + func NewLabelResolutionService() *LabelResolutionService { 356 + // Create serialization functions for our cache values 357 + serialize := func(v ResolvedLabels) ([]byte, error) { 358 + var buf bytes.Buffer 359 + err := v.MarshalCBOR(&buf) 360 + if err != nil { 361 + return nil, err 362 + } 363 + //return json.Marshal(v) 364 + return buf.Bytes(), nil 365 + } 366 + 367 + deserialize := func(data []byte) (ResolvedLabels, error) { 368 + //var result ResolvedLabels 369 + 370 + // Unmarshal from CBOR 371 + var buf bytes.Buffer = *bytes.NewBuffer(data) 372 + var labels ResolvedLabels 373 + err := labels.UnmarshalCBOR(&buf) 374 + if err != nil { 375 + return labels, err 376 + } 377 + return labels, nil 378 + //return result, json.Unmarshal(data, &result) 379 + } 380 + 381 + // Initialize the LRU cache with a capacity of 1,000,000 entries 382 + cache, err := lru.New[string]("./badger_cache", serialize, deserialize) 383 + if err != nil { 384 + log.Fatalf("Failed to initialize LRU cache: %v", err) 385 + } 386 + //cache.SetCapacity(1000000) 387 + 388 + dir := identity.DefaultDirectory() // Cached with 24hr TTL 389 + 390 + lrs := &LabelResolutionService{ 391 + incomingPromises: make(chan *QueryPromise), 392 + busQueueIn: make(chan Req, 1000), 393 + lru: cache, 394 + labelerResolver: dir, 395 + inFlightManager: NewInFlightManager(), 396 + } 397 + 398 + lrs.wg.Add(3) 399 + go lrs.runLabelResolverPipeline() 400 + go lrs.runBusQueue() 401 + // Note: runQueryLabelsHandler is implicitly part of runBusQueue's flush logic via handleBatchedRequests 402 + 403 + return lrs 404 + } 405 + 406 + func (s *LabelResolutionService) Stop() { 407 + log.Println("Stopping Label Resolution Service components...") 408 + close(s.incomingPromises) 409 + // Do not close busQueueIn immediately, let the BusQueue consumer drain it. 410 + s.wg.Wait() 411 + if err := s.lru.Close(); err != nil { 412 + log.Printf("Error closing LRU cache: %v", err) 413 + } 414 + } 415 + 416 + func (s *LabelResolutionService) HandlePromise(p *QueryPromise) { 417 + s.incomingPromises <- p 418 + } 419 + 420 + // --- 1. Label Resolver Pipeline (Handles Promises and LRU Checks) --- 421 + 422 + func (s *LabelResolutionService) runLabelResolverPipeline() { 423 + defer s.wg.Done() 424 + for p := range s.incomingPromises { 425 + s.resolvePromise(p) 426 + } 427 + } 428 + 429 + // resolvePromise handles the full lifecycle of a single incoming request. 430 + func (s *LabelResolutionService) resolvePromise(p *QueryPromise) { 431 + q := p.Query 432 + 433 + totalPairs := len(q.Subjects) * len(q.Labelers) 434 + if totalPairs == 0 { 435 + // Empty query case 436 + p.Response <- FinalResponse{Labels: make(Results)} 437 + return 438 + } 439 + 440 + tracker := NewPromiseTracker(p, totalPairs) 441 + 442 + // Phase 1: Check cache and register in-flight status 443 + s.initialCacheCheckAndQueue(p, tracker) 444 + 445 + // Phase 2: Wait for completion or timeout 446 + select { 447 + case <-p.Ctx.Done(): 448 + // Cleanup on timeout 449 + s.inFlightManager.RemovePromise(p) 450 + globalPromiseTrackers.mu.Lock() 451 + delete(globalPromiseTrackers.m, p.ID) 452 + globalPromiseTrackers.mu.Unlock() 453 + 454 + if p.Ctx.Err() == context.DeadlineExceeded { 455 + tracker.FinalizeAndRespond(p, "one of the query you asked didnt return in time, try again later") 456 + } else { 457 + tracker.FinalizeAndRespond(p, "query cancelled") 458 + } 459 + case <-tracker.Done: 460 + tracker.FinalizeAndRespond(p, "") 461 + } 462 + } 463 + 464 + func (s *LabelResolutionService) initialCacheCheckAndQueue(p *QueryPromise, tracker *PromiseTracker) { 465 + q := p.Query 466 + 467 + globalPromiseTrackers.mu.Lock() 468 + globalPromiseTrackers.m[p.ID] = tracker 469 + globalPromiseTrackers.mu.Unlock() 470 + 471 + requestsToSend := make(map[string][]string) // labelerDID -> subjects 472 + mapMu := sync.Mutex{} 473 + 474 + var wg sync.WaitGroup 475 + 476 + for _, subject := range q.Subjects { 477 + wg.Add(1) 478 + go func(subject string) { 479 + defer wg.Done() 480 + 481 + for _, labeler := range q.Labelers { 482 + cacheKey := subject + "|" + labeler 483 + 484 + value, found := s.lru.Get(cacheKey) 485 + log.Println("GET LRUKV " + cacheKey) 486 + labels := value.Labels 487 + 488 + if found { 489 + // Cache hit 490 + tracker.StoreResult(labeler, subject, labels, false) 491 + continue 492 + } 493 + 494 + // Cache miss 495 + alreadyInFlight := s.inFlightManager.CheckAndRegister(subject, labeler, p) 496 + 497 + if !alreadyInFlight { 498 + mapMu.Lock() 499 + requestsToSend[labeler] = append(requestsToSend[labeler], subject) 500 + mapMu.Unlock() 501 + } else { 502 + // Must decrement because no StoreResult will happen here 503 + tracker.TotalPairs-- 504 + } 505 + } 506 + }(subject) 507 + } 508 + wg.Wait() 509 + 510 + // 3. Send new requests to Bus Queue only for non-duplicate requests 511 + if len(requestsToSend) > 0 { 512 + subjectsToFetch := make(map[string][]string) // subject -> []labelers 513 + 514 + mapMu.Lock() 515 + for labeler, subjects := range requestsToSend { 516 + for _, subject := range subjects { 517 + subjectsToFetch[subject] = append(subjectsToFetch[subject], labeler) 518 + } 519 + } 520 + mapMu.Unlock() 521 + 522 + for subject, labelers := range subjectsToFetch { 523 + s.busQueueIn <- Req{Subject: subject, Labelers: labelers} 524 + } 525 + } 526 + } 527 + 528 + // --- 2. Bus Queue (Batching) --- 529 + 530 + func (s *LabelResolutionService) runBusQueue() { 531 + log.Println("bus queue wowww") 532 + defer s.wg.Done() 533 + 534 + // Batching pool: subject -> map[labelerDID]struct{} 535 + pool := make(map[string]map[string]struct{}) 536 + poolMu := sync.Mutex{} 537 + 538 + ticker := time.NewTicker(BusQueueBatchInterval) 539 + defer ticker.Stop() 540 + 541 + flush := func() { 542 + poolMu.Lock() 543 + defer poolMu.Unlock() 544 + 545 + if len(pool) == 0 { 546 + return 547 + } 548 + 549 + // Invert structure for Outsplitter: map[labelerDID][]subjectURI 550 + invertedRequests := make(map[string][]string) 551 + 552 + for subject, labelersMap := range pool { 553 + for labeler := range labelersMap { 554 + invertedRequests[labeler] = append(invertedRequests[labeler], subject) 555 + } 556 + } 557 + 558 + s.handleBatchedRequests(invertedRequests) 559 + pool = make(map[string]map[string]struct{}) 560 + } 561 + 562 + for { 563 + select { 564 + case req, ok := <-s.busQueueIn: 565 + if !ok { 566 + flush() 567 + return 568 + } 569 + 570 + poolMu.Lock() 571 + if _, exists := pool[req.Subject]; !exists { 572 + pool[req.Subject] = make(map[string]struct{}) 573 + } 574 + for _, labeler := range req.Labelers { 575 + pool[req.Subject][labeler] = struct{}{} 576 + } 577 + poolMu.Unlock() 578 + 579 + case <-ticker.C: 580 + flush() 581 + } 582 + } 583 + } 584 + 585 + // --- 3. Outsplitter and QueryLabels Handler --- 586 + 587 + type ExternalRequest struct { 588 + LabelerDID string 589 + Subjects []string 590 + } 591 + 592 + func (s *LabelResolutionService) handleBatchedRequests(invertedRequests map[string][]string) { 593 + var requests []ExternalRequest 594 + 595 + // Outsplitter logic: split large subject lists 596 + for labelerDID, subjects := range invertedRequests { 597 + for i := 0; i < len(subjects); i += MaxSubjectsPerExternalRequest { 598 + end := i + MaxSubjectsPerExternalRequest 599 + if end > len(subjects) { 600 + end = len(subjects) 601 + } 602 + requests = append(requests, ExternalRequest{LabelerDID: labelerDID, Subjects: subjects[i:end]}) 603 + } 604 + } 605 + 606 + var wg sync.WaitGroup 607 + for _, req := range requests { 608 + wg.Add(1) 609 + go func(r ExternalRequest) { 610 + defer wg.Done() 611 + s.executeExternalQuery(r) 612 + }(req) 613 + } 614 + wg.Wait() 615 + } 616 + 617 + func parseIntDefault(s string) (int64, error) { 618 + if s == "" { 619 + return 0, nil 620 + } 621 + return strconv.ParseInt(s, 10, 64) 622 + } 623 + 624 + func (s *LabelResolutionService) executeExternalQuery(r ExternalRequest) { 625 + log.Println("external query for " + r.LabelerDID + " " + "init") 626 + labelerDID := r.LabelerDID 627 + 628 + // 1. DID Resolution Check 629 + //domainEntry, err := s.labelerResolver.Resolve(labelerDID) 630 + did, err := syntax.ParseDID(labelerDID) 631 + if err != nil { 632 + s.handleExternalFailure(labelerDID, r.Subjects, "ResolutionFailure") // wrong did format 633 + return 634 + } 635 + ident, err := s.labelerResolver.LookupDID(context.TODO(), did) 636 + if err != nil { 637 + s.handleExternalFailure(labelerDID, r.Subjects, "ResolutionFailure") // unreachable 638 + return 639 + } 640 + log.Println("external query for " + r.LabelerDID + " " + "resolved the labeler did") 641 + 642 + labelerURL := ident.GetServiceEndpoint("atproto_labeler") 643 + if labelerURL == "" { 644 + s.handleExternalFailure(labelerDID, r.Subjects, "ResolutionFailure") // no service endpoint 645 + return 646 + } 647 + 648 + // domainEntry.Domain is the serviceEndpoint (e.g., https://mod.bsky.app) 649 + queryURL := fmt.Sprintf("%s/xrpc/com.atproto.label.queryLabels", strings.TrimSuffix(labelerURL, "/")) 650 + 651 + var allLabels []Label 652 + cursor := "" 653 + lastCursor := "-999" 654 + 655 + // 2. Query Labels Execution 656 + for page := 0; page < MaxQueryPages; page++ { 657 + 658 + if page > 0 && cursor == "" || cursor == "0" || lastCursor == cursor { 659 + break 660 + } 661 + 662 + params := url.Values{} 663 + if cursor != "" { 664 + params.Set("cursor", cursor) 665 + } 666 + for _, uri := range r.Subjects { 667 + params.Add("uriPatterns", uri) 668 + } 669 + 670 + respLabels, nextCursor, httpErr := s.queryLabels(queryURL, params) 671 + log.Println("external query for " + r.LabelerDID + " " + "querylabels at page " + strconv.Itoa(page) + " with cursor " + cursor + " and nextCursor " + nextCursor) 672 + 673 + if httpErr != nil { 674 + // Resolution was successful, but the subsequent query failed (network, 5xx, etc.) 675 + s.handleExternalFailure(labelerDID, r.Subjects, "QueryFailure") 676 + return 677 + } 678 + 679 + // Prevent null page cursor fetching (if we requested a page and got nothing) 680 + if page > 0 && len(respLabels) == 0 /*&& nextCursor == ""*/ { 681 + break 682 + } 683 + 684 + // prevent loops from faulty labelers 685 + cursorInt, errci := parseIntDefault(cursor) 686 + lastCursorInt, errlci := parseIntDefault(lastCursor) 687 + 688 + if errci != nil || errlci != nil || cursorInt <= lastCursorInt { 689 + break 690 + } 691 + 692 + allLabels = append(allLabels, respLabels...) 693 + lastCursor = cursor 694 + cursor = nextCursor 695 + 696 + if page == MaxQueryPages-1 && cursor != "" { 697 + s.handleExternalFailure(labelerDID, r.Subjects, "QueryLabelsTooManyPages") 698 + return 699 + } 700 + } 701 + 702 + log.Println("external query for " + r.LabelerDID + " " + "finished querylabels") 703 + s.handleExternalSuccess(labelerDID, r.Subjects, allLabels) 704 + } 705 + 706 + func (s *LabelResolutionService) handleExternalFailure(labelerDID string, subjects []string, failureType string) { 707 + for _, subject := range subjects { 708 + waiters := s.inFlightManager.Resolve(subject, labelerDID) 709 + 710 + for _, p := range waiters { 711 + tracker, found := globalPromiseTrackers.m[p.ID] 712 + if !found { 713 + continue 714 + } 715 + 716 + tracker.Mu.Lock() 717 + switch failureType { 718 + case "QueryLabelsTooManyPages": 719 + if !contains(tracker.Errors.QueryLabelsTooManyPages, labelerDID) { 720 + tracker.Errors.QueryLabelsTooManyPages = append(tracker.Errors.QueryLabelsTooManyPages, labelerDID) 721 + } 722 + case "ResolutionFailure": 723 + if !contains(tracker.Errors.LabelerResolutionFailure, labelerDID) { 724 + tracker.Errors.LabelerResolutionFailure = append(tracker.Errors.LabelerResolutionFailure, labelerDID) 725 + } 726 + case "QueryFailure": 727 + if !contains(tracker.Errors.LabelerQueryFailure, labelerDID) { 728 + tracker.Errors.LabelerQueryFailure = append(tracker.Errors.LabelerQueryFailure, labelerDID) 729 + } 730 + } 731 + tracker.Mu.Unlock() 732 + 733 + // Mark pair complete with empty result (failure reported in error map) 734 + tracker.StoreResult(labelerDID, subject, nil, true) 735 + } 736 + } 737 + } 738 + 739 + func (s *LabelResolutionService) handleExternalSuccess( 740 + labelerDID string, 741 + subjects []string, 742 + fetchedLabels []Label, 743 + ) { 744 + // Group labels by subject 745 + subjectLabels := make(map[string][]*Label) 746 + 747 + for i := range fetchedLabels { 748 + lbl := &fetchedLabels[i] 749 + subjectLabels[lbl.Uri] = append(subjectLabels[lbl.Uri], lbl) 750 + } 751 + 752 + for _, subject := range subjects { 753 + labels := subjectLabels[subject] 754 + 755 + resolved := ResolvedLabels{ 756 + Labels: labels, 757 + Seq: 67, // TODO: real seq tracking 758 + } 759 + 760 + cacheKey := subject + "|" + labelerDID 761 + s.lru.Put(cacheKey, resolved) 762 + log.Println("PUT LRUKV " + cacheKey) 763 + 764 + waiters := s.inFlightManager.Resolve(subject, labelerDID) 765 + 766 + for _, p := range waiters { 767 + tracker, found := globalPromiseTrackers.m[p.ID] 768 + if !found { 769 + continue 770 + } 771 + tracker.StoreResult(labelerDID, subject, labels, true) 772 + } 773 + } 774 + } 775 + 776 + func (s *LabelResolutionService) queryLabels(baseURL string, params url.Values) ([]Label, string, error) { 777 + // Build the full URL with query parameters 778 + fullURL := baseURL + "?" + params.Encode() 779 + 780 + // Create HTTP request with timeout 781 + ctx, cancel := context.WithTimeout(context.Background(), IncomingQueryTimeout) 782 + defer cancel() 783 + 784 + req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil) 785 + if err != nil { 786 + return nil, "", err 787 + } 788 + 789 + resp, err := http.DefaultClient.Do(req) 790 + if err != nil { 791 + return nil, "", err 792 + } 793 + defer resp.Body.Close() 794 + 795 + if resp.StatusCode != http.StatusOK { 796 + return nil, "", fmt.Errorf("HTTP error: %d", resp.StatusCode) 797 + } 798 + 799 + // Parse the response 800 + var result struct { 801 + Labels []Label `json:"labels"` 802 + Cursor string `json:"cursor,omitempty"` 803 + } 804 + 805 + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 806 + return nil, "", err 807 + } 808 + 809 + return result.Labels, result.Cursor, nil 810 + } 811 + 812 + func contains(s []string, e string) bool { 813 + for _, a := range s { 814 + if a == e { 815 + return true 816 + } 817 + } 818 + return false 819 + } 820 + 821 + // --- HTTP Server and Routing --- 822 + type Server struct { 823 + Service *LabelResolutionService 824 + } 825 + 826 + func (s *Server) root(c echo.Context) error { 827 + c.String(http.StatusOK, ` ____ __________ ____ _ _____ ____ ______ 828 + / __ \/ ____/ __ \ / __ \ | / / | / __ \/ ____/ 829 + / /_/ / __/ / / / / / / / / | /| / / /| | / /_/ / /_ 830 + / _, _/ /___/ /_/ / / /_/ /| |/ |/ / ___ |/ _, _/ __/ 831 + /_/ |_/_____/_____/ /_____/ |__/|__/_/ |_/_/ |_/_/ 832 + __ __ __ 833 + / /___ _/ /_ ___ / /___ ___ ___ _________ ____ 834 + / / __ '/ __ \/ _ \/ / __ '__ \/ _ \/ ___/ __ '/ _ \ 835 + / / /_/ / /_/ / __/ / / / / / / __/ / / /_/ / __/ 836 + /_/\__,_/_.___/\___/_/_/ /_/ /_/\___/_/ \__, /\___/ 837 + /____/ 838 + 839 + This is an AT Protocol cache for querying labels 840 + 841 + The only API route is /xrpc/app.reddwarf.labelmerge.queryLabels 842 + 843 + Code: https://tangled.org/whey.party/red-dwarf-server 844 + Protocol: https://atproto.com 845 + Try it on: https://reddwarf.app 846 + `) 847 + return nil 848 + } 849 + 850 + func (s *Server) queryLabels(c *gin.Context) { 851 + log.Println("NEW REQUEST FOR uhhh a bunch of stuff") 852 + 853 + var ( 854 + query IncomingQuery 855 + err error 856 + ) 857 + 858 + switch c.Request.Method { 859 + case http.MethodGet: 860 + query, err = s.parseGETQuery(c) 861 + case http.MethodPost: 862 + query, err = s.parsePOSTQuery(c) 863 + default: 864 + c.AbortWithStatus(http.StatusMethodNotAllowed) 865 + return 866 + } 867 + 868 + if err != nil { 869 + c.String(http.StatusBadRequest, "Invalid query parameters: %v", err) 870 + return 871 + } 872 + 873 + if len(query.Labelers) > MaxLabelersPerQuery || len(query.Subjects) > MaxSubjectsPerQuery { 874 + c.String( 875 + http.StatusRequestEntityTooLarge, 876 + "Query exceeds limits (Subjects: %d, Labelers: %d)", 877 + MaxSubjectsPerQuery, 878 + MaxLabelersPerQuery, 879 + ) 880 + return 881 + } 882 + 883 + query.Strict = c.Query("strict") == "true" || query.Strict 884 + 885 + // Setup context with timeout 886 + ctx, cancel := context.WithTimeout(c.Request.Context(), IncomingQueryTimeout) 887 + defer cancel() 888 + 889 + promise := &QueryPromise{ 890 + ID: strconv.FormatInt(time.Now().UnixNano(), 10), 891 + Query: query, 892 + Response: make(chan FinalResponse, 1), 893 + Ctx: ctx, 894 + } 895 + 896 + s.Service.HandlePromise(promise) 897 + 898 + select { 899 + case result := <-promise.Response: 900 + // strict mode error handling 901 + isErrorResponse := 902 + result.Error != nil && 903 + (len(result.Error.LabelerResolutionFailure) > 0 || 904 + len(result.Error.QueryLabelsTooManyPages) > 0 || 905 + len(result.Error.LabelerQueryFailure) > 0) 906 + 907 + if query.Strict && isErrorResponse { 908 + c.String(http.StatusInternalServerError, "Strict failure encountered: %v", result.Error) 909 + return 910 + } 911 + 912 + c.JSON(http.StatusOK, result) 913 + 914 + case <-ctx.Done(): 915 + timeoutMsg := "one of the query you asked didnt return in time, try again later" 916 + 917 + if query.Strict { 918 + c.String(http.StatusRequestTimeout, timeoutMsg) 919 + return 920 + } 921 + 922 + c.JSON(http.StatusOK, FinalResponse{ 923 + Error: &QueryError{ 924 + LabelerResolutionFailure: []string{timeoutMsg}, 925 + }, 926 + }) 927 + } 928 + } 929 + 930 + func (s *Server) parseGETQuery(c *gin.Context) (IncomingQuery, error) { 931 + subjects := c.QueryArray("s") 932 + labelers := c.QueryArray("l") 933 + 934 + if len(subjects) == 0 || len(labelers) == 0 { 935 + return IncomingQuery{}, fmt.Errorf("must specify subjects (s) and labelers (l)") 936 + } 937 + 938 + return IncomingQuery{ 939 + Subjects: subjects, 940 + Labelers: labelers, 941 + }, nil 942 + } 943 + 944 + func (s *Server) parsePOSTQuery(c *gin.Context) (IncomingQuery, error) { 945 + var query IncomingQuery 946 + if err := c.ShouldBindJSON(&query); err != nil { 947 + return IncomingQuery{}, err 948 + } 949 + 950 + if len(query.Subjects) == 0 || len(query.Labelers) == 0 { 951 + return IncomingQuery{}, fmt.Errorf("must specify subjects (s) and labelers (l)") 952 + } 953 + 954 + return query, nil 955 + } 956 + 957 + func main() { 958 + 959 + log.SetFlags(log.LstdFlags | log.Lshortfile) 960 + 961 + service := NewLabelResolutionService() 962 + defer service.Stop() 963 + 964 + server := &Server{Service: service} 965 + 966 + e := echo.New() 967 + //pprof.Register(e) 968 + 969 + e.Use(middleware.Recover()) 970 + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 971 + AllowOrigins: []string{"*"}, 972 + AllowMethods: []string{ 973 + http.MethodGet, 974 + http.MethodOptions, 975 + }, 976 + AllowHeaders: []string{"*"}, 977 + })) 978 + 979 + // your own routes 980 + e.GET("/", server.root) 981 + 982 + // register lexgen-generated handlers 983 + if err := server.RegisterHandlersAppReddwarfLabelmerge(e); err != nil { 984 + log.Fatalf("failed to register handlers: %v", err) 985 + } 986 + 987 + log.Println("Label Resolution Service running on", ServerPort) 988 + if err := e.Start(ServerPort); err != nil { 989 + log.Fatalf("Server failed: %v", err) 990 + } 991 + }
+44
cmd/labelmerge/stubs.go
··· 1 + package main 2 + 3 + import ( 4 + "strconv" 5 + 6 + "github.com/labstack/echo/v4" 7 + "go.opentelemetry.io/otel" 8 + appreddwarflabelmerge "tangled.org/whey.party/red-dwarf-server/labelmerge/lex" 9 + ) 10 + 11 + func (s *Server) RegisterHandlersAppReddwarfLabelmerge(e *echo.Echo) error { 12 + e.GET("/xrpc/app.reddwarf.labelmerge.queryLabels", s.HandleAppReddwarfLabelmergeQueryLabels) 13 + return nil 14 + } 15 + 16 + func (s *Server) HandleAppReddwarfLabelmergeQueryLabels(c echo.Context) error { 17 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleAppReddwarfLabelmergeQueryLabels") 18 + defer span.End() 19 + 20 + inputl := c.QueryParams()["l"] 21 + 22 + inputs := c.QueryParams()["s"] 23 + 24 + var strict *bool 25 + if p := c.QueryParam("strict"); p != "" { 26 + strict_val, err := strconv.ParseBool(p) 27 + if err != nil { 28 + return err 29 + } 30 + strict = &strict_val 31 + } 32 + var out *appreddwarflabelmerge.QueryLabels_Output 33 + var handleErr error 34 + // func (s *Server) handleAppReddwarfLabelmergeQueryLabels(ctx context.Context,l []string,s []string,strict *bool) (*appreddwarflabelmerge.QueryLabels_Output, error) 35 + out, handleErr = s.handleAppReddwarfLabelmergeQueryLabels(ctx, inputl, inputs, strict) 36 + if handleErr != nil { 37 + return handleErr 38 + } 39 + return c.JSON(200, out) 40 + } 41 + 42 + func (s *Server) RegisterHandlersComAtproto(e *echo.Echo) error { 43 + return nil 44 + }
+64
cmd/test/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log/slog" 7 + "os" 8 + "os/signal" 9 + "syscall" 10 + "time" 11 + 12 + labelstream "tangled.org/whey.party/red-dwarf-server/labelmerge/stream" 13 + ) 14 + 15 + func main() { 16 + log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 17 + Level: slog.LevelInfo, 18 + })) 19 + 20 + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 21 + defer stop() 22 + 23 + manager := labelstream.NewLabelerSubscriptionManager(log) 24 + 25 + modLabelerDID := "did:plc:ar7c4by46qjdydhdevvrndac" 26 + 27 + if err := manager.AddLabeler(modLabelerDID, "1412186"); err != nil { 28 + log.Error("Failed to add labeler", "err", err) 29 + os.Exit(1) 30 + } 31 + 32 + manager.Start() 33 + 34 + log.Info("Starting event consumption loop. Press Ctrl+C to stop.") 35 + 36 + for { 37 + select { 38 + case event, ok := <-manager.Events(): 39 + if !ok { 40 + log.Info("Event channel closed, exiting consumer.") 41 + return 42 + } 43 + 44 + action := "APPLICATION" 45 + if event.Value.Neg != nil && *event.Value.Neg { 46 + action = "NEGATION" 47 + } 48 + 49 + fmt.Printf("\n[EVENT FROM %s] Cursor: %d, Action: %s, Label: %s, URI: %s\n", 50 + event.SourceDid, 51 + event.Cursor, 52 + action, 53 + event.Value.Val, 54 + event.Value.Uri, 55 + ) 56 + 57 + case <-ctx.Done(): 58 + log.Info("Shutdown signal received.") 59 + manager.Stop() 60 + time.Sleep(1 * time.Second) 61 + return 62 + } 63 + } 64 + }
+76 -19
go.mod
··· 1 - module tangled.org/whey.party/rdcs 1 + module tangled.org/whey.party/red-dwarf-server 2 2 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-20260129212913-baa889cd148a 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 15 + github.com/labstack/echo/v4 v4.13.3 13 16 github.com/prometheus/client_golang v1.23.2 14 17 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b 15 18 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 ··· 18 21 ) 19 22 20 23 require ( 24 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect 25 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 26 + github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect 27 + github.com/dustin/go-humanize v1.0.1 // indirect 28 + github.com/gogo/protobuf v1.3.2 // indirect 29 + github.com/google/flatbuffers v25.2.10+incompatible // indirect 30 + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 31 + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 32 + github.com/hashicorp/golang-lru v1.0.2 // indirect 33 + github.com/ipfs/bbloom v0.0.4 // indirect 34 + github.com/ipfs/go-block-format v0.2.0 // indirect 35 + github.com/ipfs/go-blockservice v0.5.2 // indirect 36 + github.com/ipfs/go-datastore v0.6.0 // indirect 37 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 38 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 39 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 40 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 41 + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 42 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 43 + github.com/ipfs/go-ipld-legacy v0.2.1 // indirect 44 + github.com/ipfs/go-log v1.0.5 // indirect 45 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 46 + github.com/ipfs/go-merkledag v0.11.0 // indirect 47 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 48 + github.com/ipfs/go-verifcid v0.0.3 // indirect 49 + github.com/ipld/go-car v0.6.2 // indirect 50 + github.com/ipld/go-codec-dagpb v1.6.0 // indirect 51 + github.com/ipld/go-ipld-prime v0.21.0 // indirect 52 + github.com/jbenet/goprocess v0.1.4 // indirect 53 + github.com/jinzhu/inflection v1.0.0 // indirect 54 + github.com/jinzhu/now v1.1.5 // indirect 55 + github.com/labstack/gommon v0.4.2 // indirect 56 + github.com/lestrrat-go/blackmagic v1.0.1 // indirect 57 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 58 + github.com/lestrrat-go/httprc v1.0.4 // indirect 59 + github.com/lestrrat-go/iter v1.0.2 // indirect 60 + github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect 61 + github.com/lestrrat-go/option v1.0.1 // indirect 62 + github.com/mattn/go-colorable v0.1.14 // indirect 63 + github.com/opentracing/opentracing-go v1.2.0 // indirect 64 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 65 + github.com/segmentio/asm v1.2.0 // indirect 66 + github.com/valyala/bytebufferpool v1.0.0 // indirect 67 + github.com/valyala/fasttemplate v1.2.2 // indirect 68 + go.uber.org/atomic v1.11.0 // indirect 69 + go.uber.org/multierr v1.11.0 // indirect 70 + go.uber.org/zap v1.26.0 // indirect 71 + gorm.io/gorm v1.25.9 // indirect 72 + ) 73 + 74 + require ( 21 75 github.com/beorn7/perks v1.0.1 // indirect 76 + github.com/bluesky-social/jetstream v0.0.0-20251009222037-7d7efa58d7f1 22 77 github.com/bytedance/sonic v1.14.0 // indirect 23 78 github.com/bytedance/sonic/loader v0.3.0 // indirect 24 79 github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 80 github.com/cloudwego/base64x v0.1.6 // indirect 81 + github.com/dgraph-io/badger/v4 v4.8.0 26 82 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 27 83 github.com/felixge/httpsnoop v1.0.4 // indirect 28 - github.com/gabriel-vasile/mimetype v1.4.8 // indirect 84 + github.com/gabriel-vasile/mimetype v1.4.9 // indirect 29 85 github.com/gin-contrib/sse v1.1.0 // indirect 30 86 github.com/go-logr/logr v1.4.3 // indirect 31 87 github.com/go-logr/stdr v1.2.2 // indirect 32 88 github.com/go-playground/locales v0.14.1 // indirect 33 89 github.com/go-playground/universal-translator v0.18.1 // indirect 34 90 github.com/go-playground/validator/v10 v10.27.0 // indirect 35 - github.com/goccy/go-json v0.10.2 // indirect 91 + github.com/goccy/go-json v0.10.5 // indirect 36 92 github.com/goccy/go-yaml v1.18.0 // indirect 37 93 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 38 - github.com/ipfs/go-cid v0.4.1 // indirect 94 + github.com/ipfs/go-cid v0.5.0 39 95 github.com/json-iterator/go v1.1.12 // indirect 40 96 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 41 97 github.com/leodido/go-urn v1.4.0 // indirect ··· 52 108 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 53 109 github.com/pelletier/go-toml/v2 v2.2.4 // indirect 54 110 github.com/prometheus/client_model v0.6.2 // indirect 55 - github.com/prometheus/common v0.66.1 // indirect 56 - github.com/prometheus/procfs v0.16.1 // indirect 111 + github.com/prometheus/common v0.67.5 // indirect 112 + github.com/prometheus/procfs v0.19.2 // indirect 57 113 github.com/quic-go/qpack v0.5.1 // indirect 58 114 github.com/quic-go/quic-go v0.54.0 // indirect 59 115 github.com/spaolacci/murmur3 v1.1.0 // indirect 60 116 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 61 117 github.com/ugorji/go/codec v1.3.0 // indirect 62 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 118 + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect 119 + github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 63 120 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 64 121 go.opentelemetry.io/auto/sdk v1.1.0 // indirect 65 122 go.opentelemetry.io/otel/metric v1.38.0 // indirect 66 123 go.opentelemetry.io/otel/trace v1.38.0 // indirect 67 124 go.uber.org/mock v0.5.0 // indirect 68 - go.yaml.in/yaml/v2 v2.4.2 // indirect 125 + go.yaml.in/yaml/v2 v2.4.3 // indirect 69 126 golang.org/x/arch v0.20.0 // indirect 70 - golang.org/x/crypto v0.41.0 // indirect 71 - golang.org/x/mod v0.26.0 // indirect 72 - golang.org/x/net v0.43.0 // indirect 73 - golang.org/x/sync v0.16.0 // indirect 74 - golang.org/x/sys v0.35.0 // indirect 75 - golang.org/x/text v0.28.0 // indirect 76 - golang.org/x/tools v0.35.0 // indirect 77 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 78 - google.golang.org/protobuf v1.36.9 // indirect 79 - lukechampine.com/blake3 v1.2.1 // indirect 127 + golang.org/x/crypto v0.47.0 // indirect 128 + golang.org/x/mod v0.31.0 // indirect 129 + golang.org/x/net v0.49.0 // indirect 130 + golang.org/x/sync v0.19.0 // indirect 131 + golang.org/x/sys v0.40.0 // indirect 132 + golang.org/x/text v0.33.0 // indirect 133 + golang.org/x/tools v0.40.0 // indirect 134 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 135 + google.golang.org/protobuf v1.36.11 // indirect 136 + lukechampine.com/blake3 v1.4.1 // indirect 80 137 )
+344 -35
go.sum
··· 1 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU= 3 + github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4= 4 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 5 + github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 6 + github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 1 7 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 8 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 - github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc h1:2t+uAvfzJiCsTMwn5fW85t/IGa0+2I7BXS2ORastK4o= 4 - 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-20260129212913-baa889cd148a h1:fBniCrEkDIGVW6/6zyl72fnFOIAeeNXUF2U+L3MHaD8= 10 + github.com/bluesky-social/indigo v0.0.0-20260129212913-baa889cd148a/go.mod h1:VG/LeqLGNI3Ew7lsYixajnZGFfWPv144qbUddh+Oyag= 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/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= 36 + github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 37 + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 38 + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 16 39 github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 17 40 github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 18 41 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 h1:puGwrNTY2vCt8eakkSEq2yeNxUD3zb2kPhv1OsF1hPs= 19 42 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2/go.mod h1:ntxzdN7EhBp8h+N78AtN2hjbVKHa7mijryYd9nPMyMo= 20 43 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 21 44 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= 45 + github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 46 + github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 47 + github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 48 + github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 49 + github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= 50 + github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= 24 51 github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 25 52 github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 26 53 github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= ··· 38 65 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 39 66 github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 40 67 github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 41 - github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 68 + github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 69 + github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 70 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 42 71 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 72 + github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 73 + github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 43 74 github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 44 75 github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 76 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 77 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 45 78 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 46 79 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 80 + github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 81 + github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 47 82 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 48 83 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 49 84 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 85 + github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 86 + github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 87 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 50 88 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 51 89 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 90 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 91 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 52 92 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 53 93 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 94 + github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 95 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 96 + github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 97 + github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 98 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 99 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 100 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 101 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 54 102 github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= 55 103 github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= 56 104 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 57 105 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= 106 + github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= 107 + github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= 108 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 109 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 110 + github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= 111 + github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= 112 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 113 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 114 + github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= 115 + github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= 116 + github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 117 + github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 118 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 119 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 120 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 121 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 122 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 123 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 124 + github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= 125 + github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= 126 + github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 127 + github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 128 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 129 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 130 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= 131 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= 132 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= 133 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= 134 + github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY= 135 + github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= 136 + github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= 137 + github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= 138 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 139 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 140 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 141 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 142 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 143 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 144 + github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= 145 + github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= 146 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 147 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 148 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 149 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 150 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 151 + github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= 152 + github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= 153 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 154 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 155 + github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU= 156 + github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM= 157 + github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= 158 + github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= 159 + github.com/ipld/go-car v0.6.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc= 160 + github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8= 161 + github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= 162 + github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= 163 + github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 164 + github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 165 + github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 166 + github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 167 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 168 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 169 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 170 + github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 171 + github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 172 + github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 173 + github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 60 174 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 61 175 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 176 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 177 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 178 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 179 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 180 + github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= 181 + github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= 62 182 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 63 183 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 184 + github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= 185 + github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= 186 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 64 187 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 65 188 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 189 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 190 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 66 191 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 192 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 193 + github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 194 + github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 195 + github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 196 + github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 68 197 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 69 198 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 199 + github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= 200 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 201 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 202 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 203 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 204 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 205 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 206 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 207 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 208 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 209 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 210 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 211 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 212 + github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 213 + github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 214 + github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= 215 + github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= 216 + github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= 217 + github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= 218 + github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= 219 + github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= 220 + github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= 221 + github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= 222 + github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 223 + github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 224 + github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= 225 + github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= 226 + github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= 227 + github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= 228 + github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= 229 + github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= 230 + github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= 231 + github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= 232 + github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 233 + github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 234 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 70 235 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 71 236 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 237 + github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 238 + github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 239 + github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 240 + github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 72 241 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 73 242 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 74 243 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= ··· 82 251 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 83 252 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 84 253 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 254 + github.com/multiformats/go-multiaddr v0.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc= 255 + github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= 256 + github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= 257 + github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= 258 + github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 259 + github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 85 260 github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 86 261 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 262 + github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 263 + github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 87 264 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 88 265 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 266 + github.com/multiformats/go-multistream v0.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o= 267 + github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= 89 268 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 90 269 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 91 270 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 92 271 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 272 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 273 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 93 274 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 94 275 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 276 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 95 277 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 96 278 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 279 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 280 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 97 281 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 98 282 github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 99 283 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 100 284 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 101 - github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 102 - github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 103 - github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 104 - github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 285 + github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= 286 + github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= 287 + github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= 288 + github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 105 289 github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 106 290 github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 107 291 github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= 108 292 github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 293 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 109 294 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 110 295 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 296 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 297 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 298 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 299 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 300 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 301 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 302 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 303 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 304 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= 305 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= 111 306 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 112 307 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 113 308 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 309 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 115 310 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 311 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 116 312 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 313 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 314 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 315 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 117 316 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 118 317 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 119 318 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 319 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 120 320 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 121 321 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 122 322 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 123 323 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 124 324 github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 125 325 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= 326 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 327 + github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 328 + github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 329 + github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 330 + github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 331 + github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 332 + github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= 333 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 334 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 335 + github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 336 + github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 337 + github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 h1:W4jEGWdes35iuiiAYNZFOjx+dwzQOBh33kVpc0C0YiE= 338 + github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s= 339 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 340 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 341 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 342 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 128 343 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 129 344 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 130 345 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= ··· 143 358 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 144 359 go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 145 360 go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 361 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 362 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 363 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 364 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 365 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 146 366 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 147 367 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 148 368 go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 149 369 go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 150 - go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 151 - go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 370 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 371 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 372 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 373 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 374 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 375 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 376 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 377 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 378 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 379 + go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 380 + go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 152 381 golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 153 382 golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 154 - golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 155 - golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 156 - golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= 157 - golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 158 - golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 159 - golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 160 - golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 161 - golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 383 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 384 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 385 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 386 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 387 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 388 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 389 + golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= 390 + golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 391 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 392 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 393 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 394 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 395 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 396 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 397 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 398 + golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= 399 + golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 400 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 401 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 402 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 403 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 404 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 405 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 406 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 407 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 408 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 409 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 410 + golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= 411 + golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 412 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 413 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 416 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 417 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 418 + golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 419 + golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 420 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 421 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 426 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 427 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 428 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 429 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 430 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 431 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 432 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 - golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 164 - golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 165 - golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 166 - golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 433 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 434 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 435 + golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= 436 + golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 437 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 438 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 439 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 440 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 441 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 442 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 443 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 444 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 445 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 446 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 447 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 448 + golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= 449 + golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 167 450 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 168 451 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 169 - golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= 170 - 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= 173 - google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 174 - google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 452 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 453 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 454 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 455 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 456 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 457 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 458 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 459 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 460 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 461 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 462 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 463 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 464 + golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= 465 + golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 466 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 467 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 468 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 469 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 470 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 471 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 472 + google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 473 + google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 175 474 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 475 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 176 476 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 177 477 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 478 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 479 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 480 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 481 + gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 482 + gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 178 483 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 484 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 485 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 180 486 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= 487 + gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= 488 + gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 489 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 490 + lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= 491 + lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
+78
labelmerge/lex/generation/defs/app.reddwarf.labelmerge.queryLabels.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.reddwarf.labelmerge.queryLabels", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "s": { 12 + "type": "array", 13 + "items": { 14 + "type": "string" 15 + }, 16 + "description": "List of label subjects (strings)." 17 + }, 18 + "l": { 19 + "type": "array", 20 + "items": { 21 + "type": "string", 22 + "format": "did" 23 + }, 24 + "description": "List of label sources (labeler DIDs) to filter on." 25 + }, 26 + "strict": { 27 + "type": "boolean", 28 + "description": "If true then any errors will throw the entire query" 29 + } 30 + }, 31 + "required": [ 32 + "s", 33 + "l" 34 + ] 35 + }, 36 + "output": { 37 + "encoding": "application/json", 38 + "schema": { 39 + "type": "object", 40 + "properties": { 41 + "labels": { 42 + "type": "array", 43 + "items": { 44 + "type": "ref", 45 + "ref": "com.atproto.label.defs#label" 46 + } 47 + }, 48 + "error": { 49 + "type": "array", 50 + "items": { 51 + "type": "ref", 52 + "ref": "#error" 53 + } 54 + } 55 + }, 56 + "required": [ 57 + "labels" 58 + ] 59 + } 60 + } 61 + }, 62 + "error": { 63 + "type": "object", 64 + "properties": { 65 + "s": { 66 + "type": "string", 67 + "format": "did" 68 + }, 69 + "e": { 70 + "type": "string" 71 + } 72 + }, 73 + "required": [ 74 + "s" 75 + ] 76 + } 77 + } 78 + }
+193
labelmerge/lex/generation/external/com.atproto.label.defs.json
··· 1 + { 2 + "id": "com.atproto.label.defs", 3 + "defs": { 4 + "label": { 5 + "type": "object", 6 + "required": [ 7 + "src", 8 + "uri", 9 + "val", 10 + "cts" 11 + ], 12 + "properties": { 13 + "cid": { 14 + "type": "string", 15 + "format": "cid", 16 + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." 17 + }, 18 + "cts": { 19 + "type": "string", 20 + "format": "datetime", 21 + "description": "Timestamp when this label was created." 22 + }, 23 + "exp": { 24 + "type": "string", 25 + "format": "datetime", 26 + "description": "Timestamp at which this label expires (no longer applies)." 27 + }, 28 + "neg": { 29 + "type": "boolean", 30 + "description": "If true, this is a negation label, overwriting a previous label." 31 + }, 32 + "sig": { 33 + "type": "bytes", 34 + "description": "Signature of dag-cbor encoded label." 35 + }, 36 + "src": { 37 + "type": "string", 38 + "format": "did", 39 + "description": "DID of the actor who created this label." 40 + }, 41 + "uri": { 42 + "type": "string", 43 + "format": "uri", 44 + "description": "AT URI of the record, repository (account), or other resource that this label applies to." 45 + }, 46 + "val": { 47 + "type": "string", 48 + "maxLength": 128, 49 + "description": "The short string name of the value or type of this label." 50 + }, 51 + "ver": { 52 + "type": "integer", 53 + "description": "The AT Protocol version of the label object." 54 + } 55 + }, 56 + "description": "Metadata tag on an atproto resource (eg, repo or record)." 57 + }, 58 + "selfLabel": { 59 + "type": "object", 60 + "required": [ 61 + "val" 62 + ], 63 + "properties": { 64 + "val": { 65 + "type": "string", 66 + "maxLength": 128, 67 + "description": "The short string name of the value or type of this label." 68 + } 69 + }, 70 + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel." 71 + }, 72 + "labelValue": { 73 + "type": "string", 74 + "knownValues": [ 75 + "!hide", 76 + "!no-promote", 77 + "!warn", 78 + "!no-unauthenticated", 79 + "dmca-violation", 80 + "doxxing", 81 + "porn", 82 + "sexual", 83 + "nudity", 84 + "nsfl", 85 + "gore" 86 + ] 87 + }, 88 + "selfLabels": { 89 + "type": "object", 90 + "required": [ 91 + "values" 92 + ], 93 + "properties": { 94 + "values": { 95 + "type": "array", 96 + "items": { 97 + "ref": "#selfLabel", 98 + "type": "ref" 99 + }, 100 + "maxLength": 10 101 + } 102 + }, 103 + "description": "Metadata tags on an atproto record, published by the author within the record." 104 + }, 105 + "labelValueDefinition": { 106 + "type": "object", 107 + "required": [ 108 + "identifier", 109 + "severity", 110 + "blurs", 111 + "locales" 112 + ], 113 + "properties": { 114 + "blurs": { 115 + "type": "string", 116 + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 117 + "knownValues": [ 118 + "content", 119 + "media", 120 + "none" 121 + ] 122 + }, 123 + "locales": { 124 + "type": "array", 125 + "items": { 126 + "ref": "#labelValueDefinitionStrings", 127 + "type": "ref" 128 + } 129 + }, 130 + "severity": { 131 + "type": "string", 132 + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 133 + "knownValues": [ 134 + "inform", 135 + "alert", 136 + "none" 137 + ] 138 + }, 139 + "adultOnly": { 140 + "type": "boolean", 141 + "description": "Does the user need to have adult content enabled in order to configure this label?" 142 + }, 143 + "identifier": { 144 + "type": "string", 145 + "maxLength": 100, 146 + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 147 + "maxGraphemes": 100 148 + }, 149 + "defaultSetting": { 150 + "type": "string", 151 + "default": "warn", 152 + "description": "The default setting for this label.", 153 + "knownValues": [ 154 + "ignore", 155 + "warn", 156 + "hide" 157 + ] 158 + } 159 + }, 160 + "description": "Declares a label value and its expected interpretations and behaviors." 161 + }, 162 + "labelValueDefinitionStrings": { 163 + "type": "object", 164 + "required": [ 165 + "lang", 166 + "name", 167 + "description" 168 + ], 169 + "properties": { 170 + "lang": { 171 + "type": "string", 172 + "format": "language", 173 + "description": "The code of the language these strings are written in." 174 + }, 175 + "name": { 176 + "type": "string", 177 + "maxLength": 640, 178 + "description": "A short human-readable name for the label.", 179 + "maxGraphemes": 64 180 + }, 181 + "description": { 182 + "type": "string", 183 + "maxLength": 100000, 184 + "description": "A longer description of what the label means and why it might be applied.", 185 + "maxGraphemes": 10000 186 + } 187 + }, 188 + "description": "Strings which describe the label in the UI, localized into a specific language." 189 + } 190 + }, 191 + "$type": "com.atproto.lexicon.schema", 192 + "lexicon": 1 193 + }
+32
labelmerge/lex/generation/lexgen.json
··· 1 + [ 2 + { 3 + "package": "bsky", 4 + "prefix": "app.bsky", 5 + "outdir": "api/bsky", 6 + "import": "github.com/bluesky-social/indigo/api/bsky" 7 + }, 8 + { 9 + "package": "atproto", 10 + "prefix": "com.atproto", 11 + "outdir": "api/atproto", 12 + "import": "github.com/bluesky-social/indigo/api/atproto" 13 + }, 14 + { 15 + "package": "chat", 16 + "prefix": "chat.bsky", 17 + "outdir": "api/chat", 18 + "import": "github.com/bluesky-social/indigo/api/chat" 19 + }, 20 + { 21 + "package": "ozone", 22 + "prefix": "tools.ozone", 23 + "outdir": "api/ozone", 24 + "import": "github.com/bluesky-social/indigo/api/ozone" 25 + }, 26 + { 27 + "package": "labelmerge", 28 + "prefix": "app.reddwarf.labelmerge", 29 + "outdir": "labelmerge/lex", 30 + "import": "tangled.org/whey.party/red-dwarf-server/labelmerge/lex" 31 + } 32 + ]
+39
labelmerge/lex/generation/readme.md
··· 1 + # USING CODEGEN 2 + 3 + i dont really understand how indigo lexgen works but this two works i guess 4 + 5 + run this in the project root 6 + 7 + ### struct gen 8 + you really only need to do this when lexicon changes 9 + ```bash 10 + go run github.com/bluesky-social/indigo/cmd/lexgen/ \ 11 + --package labelmerge_lex --outdir ./labelmerge/lex \ 12 + --external-lexicons ./labelmerge/lex/generation/external/com.atproto.label.defs.json \ 13 + --build-file ./labelmerge/lex/generation/lexgen.json \ 14 + ./labelmerge/lex/generation/defs 15 + ``` 16 + 17 + ### server gen 18 + this is really only needed to be done once per project and never again. 19 + 20 + written here for future reference. 21 + ```bash 22 + go run github.com/bluesky-social/indigo/cmd/lexgen/ \ 23 + --gen-server \ 24 + --gen-handlers \ 25 + --package main \ 26 + --outdir ./cmd/labelmerge/ \ 27 + --external-lexicons ./labelmerge/lex/generation/external/ \ 28 + --types-import app.reddwarf.labelmerge:tangled.org/whey.party/red-dwarf-server/labelmerge/lex \ 29 + --types-import com.atproto:github.com/bluesky-social/indigo/api/atproto \ 30 + --build-file ./labelmerge/lex/generation/lexgen.json \ 31 + ./labelmerge/lex/defs/ 32 + ``` 33 + 34 + ### typescript client gen 35 + 36 + uses a separate tool but its kinda related so im gonna put it here too 37 + ```bash 38 + npx @atproto/lex-cli gen-api ./your/typescript/project/path ./labelmerge/lex/generation/defs/app.reddwarf.labelmerge.queryLabels.json ./labelmerge/lex/generation/external/com.atproto.label.defs.json 39 + ```
+45
labelmerge/lex/labelmergequeryLabels.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: app.reddwarf.labelmerge.queryLabels 4 + 5 + package labelmerge_lex 6 + 7 + import ( 8 + "context" 9 + 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 + ) 13 + 14 + // QueryLabels_Error is a "error" in the app.reddwarf.labelmerge.queryLabels schema. 15 + type QueryLabels_Error struct { 16 + E *string `json:"e,omitempty" cborgen:"e,omitempty"` 17 + S string `json:"s" cborgen:"s"` 18 + } 19 + 20 + // QueryLabels_Output is the output of a app.reddwarf.labelmerge.queryLabels call. 21 + type QueryLabels_Output struct { 22 + Error []*QueryLabels_Error `json:"error,omitempty" cborgen:"error,omitempty"` 23 + Labels []*comatproto.LabelDefs_Label `json:"labels" cborgen:"labels"` 24 + } 25 + 26 + // QueryLabels calls the XRPC method "app.reddwarf.labelmerge.queryLabels". 27 + // 28 + // l: List of label sources (labeler DIDs) to filter on. 29 + // s: List of label subjects (strings). 30 + // strict: If true then any errors will throw the entire query 31 + func QueryLabels(ctx context.Context, c lexutil.LexClient, l []string, s []string, strict bool) (*QueryLabels_Output, error) { 32 + var out QueryLabels_Output 33 + 34 + params := map[string]interface{}{} 35 + params["l"] = l 36 + params["s"] = s 37 + if strict { 38 + params["strict"] = strict 39 + } 40 + if err := c.LexDo(ctx, lexutil.Query, "", "app.reddwarf.labelmerge.queryLabels", params, nil, &out); err != nil { 41 + return nil, err 42 + } 43 + 44 + return &out, nil 45 + }
+150
labelmerge/lru/lru.go
··· 1 + package lru 2 + 3 + import ( 4 + "fmt" 5 + "sync" 6 + 7 + badger "github.com/dgraph-io/badger/v4" 8 + ) 9 + 10 + // Cache is a persistent, pure Badger-backed cache 11 + type Cache[K comparable, V any] struct { 12 + db *badger.DB 13 + mu sync.RWMutex // protects only internal operations, not the DB itself 14 + 15 + serialize func(V) ([]byte, error) 16 + deserialize func([]byte) (V, error) 17 + 18 + // Hooks 19 + OnAdd func(K) 20 + OnRemove func(K) 21 + } 22 + 23 + // New creates a new Badger-backed cache 24 + func New[K comparable, V any]( 25 + dbPath string, 26 + serialize func(V) ([]byte, error), 27 + deserialize func([]byte) (V, error), 28 + ) (*Cache[K, V], error) { 29 + 30 + opts := badger.DefaultOptions(dbPath).WithLogger(nil) // suppress Badger logs 31 + db, err := badger.Open(opts) 32 + if err != nil { 33 + return nil, fmt.Errorf("failed to open Badger DB: %w", err) 34 + } 35 + 36 + return &Cache[K, V]{ 37 + db: db, 38 + serialize: serialize, 39 + deserialize: deserialize, 40 + OnAdd: func(K) {}, 41 + OnRemove: func(K) {}, 42 + }, nil 43 + } 44 + 45 + // Close closes the underlying DB 46 + func (c *Cache[K, V]) Close() error { 47 + return c.db.Close() 48 + } 49 + 50 + // Get retrieves a value from Badger 51 + func (c *Cache[K, V]) Get(key K) (V, bool) { 52 + var zero V 53 + 54 + err := c.db.View(func(txn *badger.Txn) error { 55 + item, err := txn.Get([]byte(fmt.Sprintf("%v", key))) 56 + if err != nil { 57 + return err 58 + } 59 + 60 + return item.Value(func(val []byte) error { 61 + value, err := c.deserialize(val) 62 + if err != nil { 63 + return err 64 + } 65 + zero = value 66 + return nil 67 + }) 68 + }) 69 + 70 + if err == badger.ErrKeyNotFound { 71 + return zero, false 72 + } else if err != nil { 73 + return zero, false 74 + } 75 + 76 + // Trigger hook 77 + if c.OnAdd != nil { 78 + c.OnAdd(key) 79 + } 80 + return zero, true 81 + } 82 + 83 + // Put stores a value in Badger 84 + func (c *Cache[K, V]) Put(key K, value V) { 85 + serializedValue, err := c.serialize(value) 86 + if err != nil { 87 + return 88 + } 89 + 90 + err = c.db.Update(func(txn *badger.Txn) error { 91 + return txn.Set([]byte(fmt.Sprintf("%v", key)), serializedValue) 92 + }) 93 + if err != nil { 94 + return 95 + } 96 + 97 + if c.OnAdd != nil { 98 + c.OnAdd(key) 99 + } 100 + } 101 + 102 + // Remove deletes a value from Badger 103 + func (c *Cache[K, V]) Remove(key K) { 104 + _ = c.db.Update(func(txn *badger.Txn) error { 105 + return txn.Delete([]byte(fmt.Sprintf("%v", key))) 106 + }) 107 + 108 + if c.OnRemove != nil { 109 + c.OnRemove(key) 110 + } 111 + } 112 + 113 + // Len counts the number of keys in Badger 114 + func (c *Cache[K, V]) Len() int { 115 + count := 0 116 + _ = c.db.View(func(txn *badger.Txn) error { 117 + iter := txn.NewIterator(badger.DefaultIteratorOptions) 118 + defer iter.Close() 119 + 120 + for iter.Rewind(); iter.Valid(); iter.Next() { 121 + count++ 122 + } 123 + return nil 124 + }) 125 + return count 126 + } 127 + 128 + // Keys returns all keys in Badger 129 + func (c *Cache[K, V]) Keys() []K { 130 + var keys []K 131 + _ = c.db.View(func(txn *badger.Txn) error { 132 + iter := txn.NewIterator(badger.DefaultIteratorOptions) 133 + defer iter.Close() 134 + 135 + for iter.Rewind(); iter.Valid(); iter.Next() { 136 + item := iter.Item() 137 + k := item.Key() 138 + var key K 139 + fmt.Sscanf(string(k), "%v", &key) 140 + keys = append(keys, key) 141 + } 142 + return nil 143 + }) 144 + return keys 145 + } 146 + 147 + // Clear removes all keys 148 + func (c *Cache[K, V]) Clear() error { 149 + return c.db.DropAll() 150 + }
+386
labelmerge/stream/stream.go
··· 1 + package stream 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log/slog" 7 + "net/http" 8 + "net/url" 9 + "strconv" 10 + "strings" 11 + "sync" 12 + "time" 13 + 14 + comatproto "github.com/bluesky-social/indigo/api/atproto" 15 + "github.com/bluesky-social/indigo/atproto/identity" 16 + "github.com/bluesky-social/indigo/atproto/syntax" 17 + "github.com/bluesky-social/indigo/events" 18 + "github.com/bluesky-social/indigo/events/schedulers/parallel" 19 + "github.com/gorilla/websocket" 20 + ) 21 + 22 + const LabelSubscribePath = "xrpc/com.atproto.label.subscribeLabels" 23 + const MaxWorkers = 4 24 + const EventChannelCapacity = 1000 25 + 26 + // LabelEvent is the simplified, unified event object sent to the consumer. 27 + type LabelEvent struct { 28 + SourceDid string // The DID of the labeler service that sent the event 29 + Cursor int64 // The sequence number (seq) of the event 30 + Value *comatproto.LabelDefs_Label 31 + } 32 + 33 + // SubscriptionContext holds the state for a single labeler connection. 34 + type SubscriptionContext struct { 35 + DID string 36 + ServiceURL string // Resolved WSS URL 37 + CurrentCursor string // Sequence string used for connection restarts 38 + WorkerCancel context.CancelFunc 39 + WorkerCtx context.Context 40 + IsRunning bool 41 + mu sync.Mutex 42 + } 43 + 44 + // LabelerSubscriptionManager manages connections to multiple ATProto labeler services. 45 + type LabelerSubscriptionManager struct { 46 + log *slog.Logger 47 + managerCtx context.Context 48 + managerCancel context.CancelFunc 49 + wg sync.WaitGroup 50 + 51 + resolver identity.Directory // DID Resolver dependency 52 + 53 + subscriptions map[string]*SubscriptionContext 54 + subMu sync.RWMutex 55 + 56 + eventCh chan LabelEvent 57 + } 58 + 59 + // NewLabelerSubscriptionManager creates a new instance of the manager. 60 + func NewLabelerSubscriptionManager(log *slog.Logger) *LabelerSubscriptionManager { 61 + ctx, cancel := context.WithCancel(context.Background()) 62 + dir := identity.DefaultDirectory() // Cached with 24hr TTL 63 + return &LabelerSubscriptionManager{ 64 + log: log, 65 + managerCtx: ctx, 66 + managerCancel: cancel, 67 + subscriptions: make(map[string]*SubscriptionContext), 68 + eventCh: make(chan LabelEvent, EventChannelCapacity), 69 + resolver: dir, 70 + } 71 + } 72 + 73 + // Events returns the read-only channel where all aggregated LabelEvents are sent. 74 + func (m *LabelerSubscriptionManager) Events() <-chan LabelEvent { 75 + return m.eventCh 76 + } 77 + 78 + // Start initiates the subscription manager. It attempts to resolve the URL and start workers 79 + // for all currently added labelers. 80 + func (m *LabelerSubscriptionManager) Start() { 81 + m.subMu.RLock() 82 + defer m.subMu.RUnlock() 83 + 84 + m.log.Info("Starting Labeler Subscription Manager", "count", len(m.subscriptions)) 85 + 86 + for _, sub := range m.subscriptions { 87 + // Initial resolution check (the worker loop handles retries) 88 + did, err1 := syntax.ParseDID(sub.DID) 89 + if err1 != nil { 90 + m.log.Warn(sub.DID + "ResolutionFailure invalid did") 91 + } else { 92 + ident, err2 := m.resolver.LookupDID(context.TODO(), did) 93 + if err2 != nil { 94 + m.log.Warn(sub.DID + "ResolutionFailure not reachable") 95 + } else { 96 + labelerURL := ident.GetServiceEndpoint("atproto_labeler") 97 + if labelerURL == "" { 98 + m.log.Warn(sub.DID + "ResolutionFailure no service endpoint") 99 + } else { 100 + sub.ServiceURL = labelerURL 101 + m.log.Info("Initial DID resolved successfully", "did", sub.DID, "service_url", sub.ServiceURL) 102 + } 103 + } 104 + } 105 + m.startWorker(sub) 106 + } 107 + } 108 + 109 + // Stop gracefully shuts down all active connections and the manager. 110 + func (m *LabelerSubscriptionManager) Stop() { 111 + m.managerCancel() 112 + m.log.Info("Waiting for all labeler workers to shut down...") 113 + m.wg.Wait() 114 + m.log.Info("Manager stopped gracefully.") 115 + close(m.eventCh) 116 + } 117 + 118 + // AddLabeler registers a new labeler DID. URL resolution is handled lazily (in Start or in the worker loop). 119 + func (m *LabelerSubscriptionManager) AddLabeler(did string, cursor string) error { 120 + if did == "" { 121 + return fmt.Errorf("did cannot be empty") 122 + } 123 + 124 + m.subMu.Lock() 125 + defer m.subMu.Unlock() 126 + 127 + if _, exists := m.subscriptions[did]; exists { 128 + m.log.Warn("Labeler already exists", "did", did) 129 + return nil 130 + } 131 + 132 + workerCtx, workerCancel := context.WithCancel(m.managerCtx) 133 + 134 + sub := &SubscriptionContext{ 135 + DID: did, 136 + ServiceURL: "", // Must be resolved before use 137 + CurrentCursor: cursor, 138 + WorkerCtx: workerCtx, 139 + WorkerCancel: workerCancel, 140 + IsRunning: false, 141 + } 142 + 143 + m.subscriptions[did] = sub 144 + 145 + // If the overall manager is running, try to resolve and start the worker immediately. 146 + select { 147 + case <-m.managerCtx.Done(): 148 + // Manager is shutting down, just register the sub but don't start 149 + default: 150 + // Attempt immediate resolution and start 151 + // entry, err := m.resolver.Resolve(sub.DID) 152 + // if err == nil { 153 + // sub.ServiceURL = entry.Domain 154 + // m.log.Info("Immediate DID resolution successful for new labeler", "did", sub.DID, "service_url", sub.ServiceURL) 155 + // } else { 156 + // m.log.Warn("Immediate DID resolution failed for new labeler. Worker will retry.", "did", sub.DID, "err", err) 157 + // } 158 + // m.startWorker(sub) 159 + did, err1 := syntax.ParseDID(sub.DID) 160 + if err1 != nil { 161 + m.log.Warn(sub.DID + "ResolutionFailure invalid did") 162 + } else { 163 + ident, err2 := m.resolver.LookupDID(context.TODO(), did) 164 + if err2 != nil { 165 + m.log.Warn(sub.DID + "ResolutionFailure not reachable") 166 + } else { 167 + labelerURL := ident.GetServiceEndpoint("atproto_labeler") 168 + if labelerURL == "" { 169 + m.log.Warn(sub.DID + "ResolutionFailure no service endpoint") 170 + } else { 171 + sub.ServiceURL = labelerURL 172 + m.log.Info("Initial DID resolved successfully", "did", sub.DID, "service_url", sub.ServiceURL) 173 + } 174 + } 175 + } 176 + m.startWorker(sub) 177 + } 178 + 179 + m.log.Info("Labeler added", "did", did, "start_cursor", cursor) 180 + return nil 181 + } 182 + 183 + // RemoveLabeler stops the stream worker for the given DID and removes it from the manager. 184 + func (m *LabelerSubscriptionManager) RemoveLabeler(did string) { 185 + m.subMu.Lock() 186 + defer m.subMu.Unlock() 187 + 188 + sub, exists := m.subscriptions[did] 189 + if !exists { 190 + return 191 + } 192 + 193 + m.log.Info("Removing labeler subscription", "did", did) 194 + sub.WorkerCancel() 195 + delete(m.subscriptions, did) 196 + } 197 + 198 + // startWorker spins up the connection management goroutine for a single labeler. 199 + func (m *LabelerSubscriptionManager) startWorker(sub *SubscriptionContext) { 200 + sub.mu.Lock() 201 + if sub.IsRunning { 202 + sub.mu.Unlock() 203 + return 204 + } 205 + sub.IsRunning = true 206 + sub.mu.Unlock() 207 + 208 + m.wg.Add(1) 209 + go func() { 210 + defer m.wg.Done() 211 + defer func() { 212 + sub.mu.Lock() 213 + sub.IsRunning = false 214 + sub.mu.Unlock() 215 + }() 216 + m.manageSubscription(sub) 217 + }() 218 + } 219 + 220 + // manageSubscription handles connection retries, cursors, and running the stream processor. 221 + func (m *LabelerSubscriptionManager) manageSubscription(sub *SubscriptionContext) { 222 + didLog := m.log.With("did", sub.DID) 223 + didLog.Info("Worker started") 224 + 225 + for { 226 + select { 227 + case <-sub.WorkerCtx.Done(): 228 + didLog.Info("Worker received stop signal, shutting down.") 229 + return 230 + default: 231 + // Proceed 232 + } 233 + var err error 234 + 235 + // 1. Ensure ServiceURL is resolved 236 + if sub.ServiceURL == "" { 237 + didLog.Info("Service URL not resolved, attempting DID resolution.") 238 + // entry, err := m.resolver.Resolve(sub.DID) 239 + // if err != nil { 240 + // didLog.Error("DID resolution failed, retrying...", "err", err) 241 + // goto WaitAndRetry 242 + // } 243 + // sub.ServiceURL = entry.Domain 244 + // didLog.Info("DID resolution successful", "service_url", sub.ServiceURL) 245 + did, err1 := syntax.ParseDID(sub.DID) 246 + if err1 != nil { 247 + m.log.Warn(sub.DID + "ResolutionFailure invalid did") 248 + } else { 249 + ident, err2 := m.resolver.LookupDID(context.TODO(), did) 250 + if err2 != nil { 251 + m.log.Warn(sub.DID + "ResolutionFailure not reachable") 252 + goto WaitAndRetry 253 + } else { 254 + labelerURL := ident.GetServiceEndpoint("atproto_labeler") 255 + if labelerURL == "" { 256 + m.log.Warn(sub.DID + "ResolutionFailure no service endpoint") 257 + } else { 258 + sub.ServiceURL = labelerURL 259 + m.log.Info("Initial DID resolved successfully", "did", sub.DID, "service_url", sub.ServiceURL) 260 + } 261 + } 262 + } 263 + } 264 + 265 + // 2. Attempt to stream 266 + err = m.dialAndStream(sub) 267 + 268 + if sub.WorkerCtx.Err() != nil { 269 + return 270 + } 271 + 272 + if err != nil { 273 + didLog.Error("Stream failed, attempting restart", "err", err, "cursor", sub.CurrentCursor) 274 + } else { 275 + didLog.Info("Stream closed cleanly, attempting restart.") 276 + } 277 + 278 + WaitAndRetry: 279 + // Wait before retrying 280 + didLog.Info("Waiting 5s before reconnecting/retrying resolution...") 281 + select { 282 + case <-time.After(5 * time.Second): 283 + // Proceed with retry 284 + case <-sub.WorkerCtx.Done(): 285 + return // Exit if canceled during wait 286 + } 287 + } 288 + } 289 + 290 + func httpToWS(s string) string { 291 + if strings.HasPrefix(s, "https://") { 292 + return "wss://" + s[len("https://"):] 293 + } 294 + if strings.HasPrefix(s, "http://") { 295 + return "ws://" + s[len("http://"):] 296 + } 297 + return s 298 + } 299 + 300 + // dialAndStream establishes the WebSocket connection and processes the stream. 301 + func (m *LabelerSubscriptionManager) dialAndStream(sub *SubscriptionContext) error { 302 + didLog := m.log.With("did", sub.DID) 303 + 304 + fullURL := httpToWS(sub.ServiceURL) + "/" + LabelSubscribePath 305 + if sub.CurrentCursor != "" { 306 + fullURL = fmt.Sprintf("%s?cursor=%s", fullURL, sub.CurrentCursor) 307 + } 308 + 309 + u, err := url.Parse(fullURL) 310 + if err != nil { 311 + return fmt.Errorf("failed to parse URL: %w", err) 312 + } 313 + 314 + // 1. Establish WebSocket Connection 315 + dialer := websocket.DefaultDialer 316 + con, resp, err := dialer.Dial(u.String(), http.Header{ 317 + "User-Agent": []string{"LabelerSubscriptionManager/1.0"}, 318 + }) 319 + 320 + if err != nil { 321 + if resp != nil { 322 + didLog.Error("WebSocket connection failed", "status", resp.StatusCode) 323 + } 324 + // If dial fails due to network/DNS/TLS error, clear the service URL to force re-resolution 325 + // on the next loop iteration. 326 + sub.mu.Lock() 327 + sub.ServiceURL = "" 328 + sub.mu.Unlock() 329 + 330 + return fmt.Errorf("failed to dial websocket to %s: %w", u.String(), err) 331 + } 332 + defer con.Close() 333 + didLog.Info("Successfully connected to Labeler firehose", "url", u.String()) 334 + 335 + // 2. Define Event Callbacks 336 + rsc := &events.RepoStreamCallbacks{ 337 + LabelLabels: func(evt *comatproto.LabelSubscribeLabels_Labels) error { 338 + if evt.Seq == 0 || evt.Labels == nil { 339 + return nil 340 + } 341 + 342 + // Update the cursor immediately 343 + sub.mu.Lock() 344 + sub.CurrentCursor = strconv.FormatInt(evt.Seq, 10) 345 + sub.mu.Unlock() 346 + 347 + // Process and simplify each label event 348 + for _, label := range evt.Labels { 349 + select { 350 + case m.eventCh <- LabelEvent{ 351 + SourceDid: sub.DID, 352 + Cursor: evt.Seq, 353 + Value: label, 354 + }: 355 + // Sent successfully 356 + default: 357 + didLog.Warn("Event channel full, dropping label event", "seq", evt.Seq, "uri", label.Uri) 358 + } 359 + } 360 + return nil 361 + }, 362 + 363 + LabelInfo: func(evt *comatproto.LabelSubscribeLabels_Info) error { 364 + if evt.Message != nil { 365 + didLog.Info("Stream Info Message", "name", evt.Name, "message", *evt.Message) 366 + } 367 + return nil 368 + }, 369 + 370 + Error: func(evt *events.ErrorFrame) error { 371 + didLog.Error("Stream processing error frame", "error_type", evt.Error, "message", evt.Message) 372 + return fmt.Errorf("atproto stream error: %s", evt.Message) 373 + }, 374 + } 375 + 376 + // 3. Create Scheduler and Start Processing 377 + scheduler := parallel.NewScheduler( 378 + MaxWorkers, 379 + EventChannelCapacity, 380 + fullURL, 381 + rsc.EventHandler, 382 + ) 383 + 384 + // HandleRepoStream blocks until the connection closes or the context cancels. 385 + return events.HandleRepoStream(sub.WorkerCtx, con, scheduler, didLog) 386 + }
+22
license
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Whey and contributors. 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE. 22 +
-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/rdcs/microcosm/constellation" 12 - "tangled.org/whey.party/rdcs/microcosm/slingshot" 13 - "tangled.org/whey.party/rdcs/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, "RDCS 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 - }
+1 -1
microcosm/constellation/constellation.go
··· 4 4 "context" 5 5 6 6 "github.com/bluesky-social/indigo/lex/util" 7 - "tangled.org/whey.party/rdcs/microcosm" 7 + "tangled.org/whey.party/red-dwarf-server/microcosm" 8 8 ) 9 9 10 10 func NewConstellation(host string) *microcosm.MicrocosmClient {
+1 -1
microcosm/microcosm.go
··· 31 31 Client: http.DefaultClient, 32 32 Host: host, 33 33 Headers: map[string][]string{ 34 - "User-Agent": []string{"microcosm-rdcs"}, 34 + "User-Agent": []string{"microcosm-red-dwarf-server"}, 35 35 }, 36 36 } 37 37 }
+1 -1
microcosm/slingshot/slingshot.go
··· 5 5 6 6 "github.com/bluesky-social/indigo/api/agnostic" 7 7 "github.com/bluesky-social/indigo/lex/util" 8 - "tangled.org/whey.party/rdcs/microcosm" 8 + "tangled.org/whey.party/red-dwarf-server/microcosm" 9 9 ) 10 10 11 11 func NewSlingshot(host string) *microcosm.MicrocosmClient {
+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 + }
+85
readme.md
··· 1 + # Red Dwarf Server 2 + 3 + Golang Monorepo for all server-sided Red Dwarf stuff 4 + 5 + > [!NOTE] 6 + > uh im not very confident with the current directory structure, so files and folders might move around 7 + 8 + ## Runnables 9 + run all of these using `go run .` inside the respective directories 10 + 11 + ### `/cmd/appview` 12 + an appview, the api server that implements app.bsky.* XRPC methods. 13 + 14 + development of the appview itself is on hold, but other parts of this red dwarf server repo are still being developed and will be used by the appview, probably, eventually 15 + 16 + still very early in development 17 + 18 + implemented routes: 19 + - `app.bsky.actor.getProfiles` 20 + - `app.bsky.actor.getProfile` 21 + - `app.bsky.notification.listNotifications` (placeholder) 22 + - `app.bsky.labeler.getServices` 23 + - `app.bsky.feed.getFeedGenerators` 24 + - `app.bsky.feed.getPosts` (post rendering is incomplete) 25 + - `app.bsky.feed.getFeed` (post rendering is incomplete) 26 + - `app.bsky.unspecced.getConfig` (placeholder) 27 + - `app.bsky.unspecced.getPostThreadV2` (mostly working! doesnt use prefered sort, not performant yet) 28 + 29 + 30 + ### `/cmd/labelmerge` 31 + queryLabel cache. uses a different XRPC method than the default queryLabels endpoint 32 + 33 + - `/xrpc/app.reddwarf.labelmerge.queryLabels` 34 + 35 + the full lexicon schema is [here](/labelmerge/lex/generation/defs/app.reddwarf.labelmerge.queryLabels.json) 36 + 37 + ### `/cmd/backstream` 38 + experimental backfiller that kinda (but not really) conforms to the jetstream event shape. designed to be ingested by consumers expecting jetstream 39 + 40 + ### `/cmd/aturilist` 41 + 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) 42 + 43 + ## Packages 44 + 45 + ### `/auth` 46 + taken from [go-bsky-feed-generator](https://github.com/jazware/go-bsky-feed-generator) but modified a bit. 47 + 48 + handles all of the auth, modified to have a more lenient version to make `getFeed` work 49 + 50 + ### `/microcosm/*` 51 + microcosm api clients, implements constellation slingshot and spacedust 52 + 53 + slingshot's api client is compatible with `github.com/bluesky-social/indigo/*` stuff, like `agnostic.RepoGetRecord` and `util.LexClient` 54 + 55 + ### `/labelmerge/*` 56 + labelmerge helpers, like: 57 + - a badger LRU (of unknown reliability) 58 + - labeler firehose ingester manager (still WIP, and unused) 59 + - lexicon generation files and the generated structs 60 + 61 + ### `/shims/*` 62 + 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` 63 + 64 + 65 + ### `/sticket` 66 + unused leftover sorry 67 + 68 + 69 + ### `/store` 70 + unused leftover sorry 71 + 72 + ## todo 73 + 74 + - implement the many other parts of labelmerge 75 + - labeler firehose ingester, which will be used for: 76 + - keep caches up to date via firehose ingester 77 + - rolling cache of the latest few hours of labels 78 + - which will need a "was this record made in the latest few hours of labels" helper service 79 + - clean up /cmd/appview/main.go , its a mess 80 + - appview-side query caches 81 + - notification service 82 + - bookmarks service 83 + - create aturilist service 84 + - make backstream usable 85 + - 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 + }