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

Compare changes

Choose any two refs to compare.

Changed files
+4364 -966
backstream
cmd
appview
aturilist
backstream
jetrelay
shims
lex
app
bsky
actor
feed
unspecced
getpostthreadv2
utils
+2
.gitignore
··· 1 + cmd/aturilist/badger_data 2 + cmd/backstream/temp
+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 + }
+40 -7
go.mod
··· 3 3 go 1.25.4 4 4 5 5 require ( 6 - github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc 6 + github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635 7 7 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 8 + github.com/gin-contrib/cors v1.7.6 8 9 github.com/gin-gonic/gin v1.11.0 9 10 github.com/golang-jwt/jwt v3.2.2+incompatible 10 11 github.com/google/uuid v1.6.0 11 12 github.com/gorilla/websocket v1.5.3 12 13 github.com/hashicorp/golang-lru/arc/v2 v2.0.7 14 + github.com/klauspost/compress v1.18.2 13 15 github.com/prometheus/client_golang v1.23.2 14 16 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b 15 17 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 ··· 18 20 ) 19 21 20 22 require ( 21 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 22 - github.com/gin-contrib/cors v1.7.6 // indirect 23 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 24 + github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect 25 + github.com/dustin/go-humanize v1.0.1 // indirect 26 + github.com/gogo/protobuf v1.3.2 // indirect 27 + github.com/google/flatbuffers v25.2.10+incompatible // indirect 28 + github.com/hashicorp/golang-lru v1.0.2 // indirect 29 + github.com/ipfs/bbloom v0.0.4 // indirect 30 + github.com/ipfs/go-block-format v0.2.0 // indirect 31 + github.com/ipfs/go-blockservice v0.5.2 // indirect 32 + github.com/ipfs/go-datastore v0.6.0 // indirect 33 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 34 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 35 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 36 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 37 + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 38 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 39 + github.com/ipfs/go-ipld-legacy v0.2.1 // indirect 40 + github.com/ipfs/go-log v1.0.5 // indirect 41 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 42 + github.com/ipfs/go-merkledag v0.11.0 // indirect 43 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 44 + github.com/ipfs/go-verifcid v0.0.3 // indirect 45 + github.com/ipld/go-car v0.6.2 // indirect 46 + github.com/ipld/go-codec-dagpb v1.6.0 // indirect 47 + github.com/ipld/go-ipld-prime v0.21.0 // indirect 48 + github.com/jbenet/goprocess v0.1.4 // indirect 23 49 github.com/lestrrat-go/blackmagic v1.0.1 // indirect 24 50 github.com/lestrrat-go/httpcc v1.0.1 // indirect 25 51 github.com/lestrrat-go/httprc v1.0.4 // indirect 26 52 github.com/lestrrat-go/iter v1.0.2 // indirect 27 53 github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect 28 54 github.com/lestrrat-go/option v1.0.1 // indirect 55 + github.com/opentracing/opentracing-go v1.2.0 // indirect 56 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 29 57 github.com/segmentio/asm v1.2.0 // indirect 58 + go.uber.org/atomic v1.11.0 // indirect 59 + go.uber.org/multierr v1.11.0 // indirect 60 + go.uber.org/zap v1.26.0 // indirect 30 61 ) 31 62 32 63 require ( 33 64 github.com/beorn7/perks v1.0.1 // indirect 65 + github.com/bluesky-social/jetstream v0.0.0-20251009222037-7d7efa58d7f1 34 66 github.com/bytedance/sonic v1.14.0 // indirect 35 67 github.com/bytedance/sonic/loader v0.3.0 // indirect 36 68 github.com/cespare/xxhash/v2 v2.3.0 // indirect 37 69 github.com/cloudwego/base64x v0.1.6 // indirect 70 + github.com/dgraph-io/badger/v4 v4.8.0 38 71 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 39 72 github.com/felixge/httpsnoop v1.0.4 // indirect 40 73 github.com/gabriel-vasile/mimetype v1.4.9 // indirect ··· 47 80 github.com/goccy/go-json v0.10.5 // indirect 48 81 github.com/goccy/go-yaml v1.18.0 // indirect 49 82 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 50 - github.com/ipfs/go-cid v0.4.1 // indirect 83 + github.com/ipfs/go-cid v0.5.0 51 84 github.com/json-iterator/go v1.1.12 // indirect 52 85 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 53 86 github.com/leodido/go-urn v1.4.0 // indirect ··· 71 104 github.com/spaolacci/murmur3 v1.1.0 // indirect 72 105 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 73 106 github.com/ugorji/go/codec v1.3.0 // indirect 74 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 107 + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect 75 108 github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 76 109 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 77 110 go.opentelemetry.io/auto/sdk v1.1.0 // indirect ··· 87 120 golang.org/x/sys v0.35.0 // indirect 88 121 golang.org/x/text v0.28.0 // indirect 89 122 golang.org/x/tools v0.35.0 // indirect 90 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 123 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 91 124 google.golang.org/protobuf v1.36.9 // indirect 92 - lukechampine.com/blake3 v1.2.1 // indirect 125 + lukechampine.com/blake3 v1.4.1 // indirect 93 126 )
+229 -12
go.sum
··· 1 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 + github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 4 + github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 1 5 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 6 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 7 github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc h1:2t+uAvfzJiCsTMwn5fW85t/IGa0+2I7BXS2ORastK4o= 4 8 github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= 9 + github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635 h1:kNeRrgGJH2g5OvjLqtaQ744YXqduliZYpFkJ/ld47c0= 10 + github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= 11 + github.com/bluesky-social/jetstream v0.0.0-20251009222037-7d7efa58d7f1 h1:ovcRKN1iXZnY5WApVg+0Hw2RkwMH0ziA7lSAA8vellU= 12 + github.com/bluesky-social/jetstream v0.0.0-20251009222037-7d7efa58d7f1/go.mod h1:5PtGi4r/PjEVBBl+0xWuQn4mBEjr9h6xsfDBADS6cHs= 5 13 github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= 6 14 github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= 7 15 github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= ··· 10 18 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 19 github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 12 20 github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 21 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 22 + github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 23 + github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 13 24 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 25 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 26 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 27 github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 17 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 18 28 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 29 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 30 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 31 + github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= 32 + github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= 33 + github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= 34 + github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= 35 + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 36 + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 19 37 github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 20 38 github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 21 39 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 h1:puGwrNTY2vCt8eakkSEq2yeNxUD3zb2kPhv1OsF1hPs= 22 40 github.com/ericvolp12/jwt-go-secp256k1 v0.0.2/go.mod h1:ntxzdN7EhBp8h+N78AtN2hjbVKHa7mijryYd9nPMyMo= 23 41 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 24 42 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 25 - github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 26 - github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 43 + github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 44 + github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 27 45 github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 28 46 github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 29 47 github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= ··· 45 63 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 46 64 github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 47 65 github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 48 - github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 66 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 49 67 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 50 68 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 51 69 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 52 70 github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 53 71 github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 72 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 73 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 54 74 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 55 75 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 76 + github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 77 + github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 56 78 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 57 79 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 58 80 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 + github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 82 + github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 83 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 59 84 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 60 85 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 86 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 87 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 61 88 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 62 89 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 90 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 91 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 63 92 github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= 64 93 github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= 65 94 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 66 95 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 67 - github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 68 - github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 96 + github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= 97 + github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= 98 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 99 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 100 + github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= 101 + github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= 102 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 103 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 104 + github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= 105 + github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= 106 + github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 107 + github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 108 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 109 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 110 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 111 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 112 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 113 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 114 + github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= 115 + github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= 116 + github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 117 + github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 118 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 119 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 120 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= 121 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= 122 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= 123 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= 124 + github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY= 125 + github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= 126 + github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= 127 + github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= 128 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 129 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 130 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 131 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 132 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 133 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 134 + github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= 135 + github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= 136 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 137 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 138 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 139 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 140 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 141 + github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= 142 + github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= 143 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 144 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 145 + github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU= 146 + github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM= 147 + github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= 148 + github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= 149 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 h1:oFo19cBmcP0Cmg3XXbrr0V/c+xU9U1huEZp8+OgBzdI= 150 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4/go.mod h1:6nkFF8OmR5wLKBzRKi7/YFJpyYR7+oEn1DX+mMWnlLA= 151 + github.com/ipld/go-car v0.6.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc= 152 + github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8= 153 + github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= 154 + github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= 155 + github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 156 + github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 157 + github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 158 + github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 159 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 160 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 161 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 69 162 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 70 163 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 164 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 165 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 166 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 167 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 168 + github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= 169 + github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= 71 170 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 72 171 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 172 + github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= 173 + github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= 174 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 73 175 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 74 176 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 177 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 178 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 75 179 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 76 180 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 77 181 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= ··· 89 193 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 90 194 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 91 195 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 196 + github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 197 + github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 198 + github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= 199 + github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= 200 + github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= 201 + github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= 202 + github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= 203 + github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= 204 + github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= 205 + github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= 206 + github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 207 + github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 208 + github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= 209 + github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= 210 + github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= 211 + github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= 212 + github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= 213 + github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= 214 + github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= 215 + github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= 216 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 92 217 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 93 218 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 219 + github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 220 + github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 221 + github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 222 + github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 94 223 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 95 224 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 96 225 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= ··· 104 233 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 105 234 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 106 235 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 236 + github.com/multiformats/go-multiaddr v0.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc= 237 + github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= 238 + github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= 239 + github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= 240 + github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 241 + github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 107 242 github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 108 243 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 244 + github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 245 + github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 109 246 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 110 247 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 248 + github.com/multiformats/go-multistream v0.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o= 249 + github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= 111 250 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 112 251 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 113 252 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 114 253 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 254 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 255 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 115 256 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 116 257 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 258 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 117 259 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 118 260 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 261 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 262 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 119 263 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 120 264 github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 121 265 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= ··· 128 272 github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 129 273 github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= 130 274 github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 275 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 131 276 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 132 277 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 278 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 133 279 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 134 280 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 281 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 282 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 283 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 284 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 285 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 286 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= 287 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= 135 288 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 136 289 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 137 290 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 138 291 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 139 292 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 140 293 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 294 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 141 295 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 296 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 142 297 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 298 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 144 299 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= ··· 149 304 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 150 305 github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 151 306 github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 152 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 153 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 307 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 308 + github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 309 + github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= 310 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 311 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 312 + github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 313 + github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 154 314 github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371 h1:W4jEGWdes35iuiiAYNZFOjx+dwzQOBh33kVpc0C0YiE= 155 315 github.com/whyrusleeping/go-did v0.0.0-20240828165449-bcaa7ae21371/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s= 316 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 317 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 318 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 156 319 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 157 320 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 158 321 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= ··· 172 335 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 173 336 go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 174 337 go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 338 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 339 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 340 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 341 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 342 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 175 343 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 176 344 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 177 345 go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 178 346 go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 347 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 348 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 349 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 350 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 351 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 352 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 353 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 354 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 355 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 179 356 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 180 357 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 181 358 golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 182 359 golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 183 360 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 361 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 362 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 363 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 184 364 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 185 365 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 186 366 golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 187 367 golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 368 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 369 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 370 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 371 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 372 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 188 373 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 189 374 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 190 375 golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= 191 376 golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 377 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 378 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 192 379 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 380 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 381 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 193 382 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 383 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 194 384 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 195 385 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 196 386 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 197 387 golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 198 388 golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 199 389 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 390 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 391 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 392 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 393 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 394 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 395 golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 203 396 golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 204 397 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 398 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 400 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 206 403 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 404 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 207 405 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 208 406 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 209 407 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 229 427 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 230 428 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 231 429 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 430 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 431 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 432 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 433 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 434 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 232 435 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 436 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 437 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 438 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 233 439 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 234 440 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 235 441 golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= 236 442 golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 237 443 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 238 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 239 - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 444 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 445 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 446 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 447 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 448 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 240 449 google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 241 450 google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 242 451 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 452 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 243 453 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 244 454 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 455 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 456 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 457 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 458 + gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 459 + gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 245 460 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 461 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 246 462 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 247 463 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 248 - lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 249 - lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 464 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 465 + lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= 466 + lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
-794
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 - "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 20 - "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 21 - appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs" 22 - appbskyfeeddefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/feed/defs" 23 - "tangled.org/whey.party/red-dwarf-server/shims/utils" 24 - "tangled.org/whey.party/red-dwarf-server/sticket" 25 - "tangled.org/whey.party/red-dwarf-server/store" 26 - 27 - // "github.com/bluesky-social/indigo/atproto/atclient" 28 - // comatproto "github.com/bluesky-social/indigo/api/atproto" 29 - appbsky "github.com/bluesky-social/indigo/api/bsky" 30 - "github.com/bluesky-social/indigo/atproto/syntax" 31 - 32 - // "github.com/bluesky-social/indigo/atproto/atclient" 33 - // "github.com/bluesky-social/indigo/atproto/identity" 34 - // "github.com/bluesky-social/indigo/atproto/syntax" 35 - "github.com/bluesky-social/indigo/api/agnostic" 36 - "github.com/gin-contrib/cors" 37 - "github.com/gin-gonic/gin" 38 - // "github.com/bluesky-social/jetstream/pkg/models" 39 - ) 40 - 41 - var ( 42 - JETSTREAM_URL string 43 - SPACEDUST_URL string 44 - SLINGSHOT_URL string 45 - CONSTELLATION_URL string 46 - ) 47 - 48 - func initURLs(prod bool) { 49 - if !prod { 50 - JETSTREAM_URL = "wss://jetstream.whey.party/subscribe" 51 - SPACEDUST_URL = "wss://spacedust.whey.party/subscribe" 52 - SLINGSHOT_URL = "https://slingshot.whey.party" 53 - CONSTELLATION_URL = "https://constellation.whey.party" 54 - } else { 55 - JETSTREAM_URL = "ws://localhost:6008/subscribe" 56 - SPACEDUST_URL = "ws://localhost:9998/subscribe" 57 - SLINGSHOT_URL = "http://localhost:7729" 58 - CONSTELLATION_URL = "http://localhost:7728" 59 - } 60 - } 61 - 62 - const ( 63 - BSKYIMAGECDN_URL = "https://cdn.bsky.app" 64 - BSKYVIDEOCDN_URL = "https://video.bsky.app" 65 - serviceWebDID = "did:web:server.reddwarf.app" 66 - serviceWebHost = "https://server.reddwarf.app" 67 - ) 68 - 69 - func main() { 70 - log.Println("red-dwarf-server started") 71 - prod := flag.Bool("prod", false, "use production URLs instead of localhost") 72 - flag.Parse() 73 - 74 - initURLs(*prod) 75 - 76 - ctx := context.Background() 77 - mailbox := sticket.New() 78 - sl := slingshot.NewSlingshot(SLINGSHOT_URL) 79 - cs := constellation.NewConstellation(CONSTELLATION_URL) 80 - // spacedust is type definitions only 81 - // jetstream types is probably available from jetstream/pkg/models 82 - 83 - router_raw := gin.New() 84 - router_raw.Use(gin.Logger()) 85 - router_raw.Use(gin.Recovery()) 86 - router_raw.Use(cors.Default()) 87 - 88 - router_raw.GET("/.well-known/did.json", GetWellKnownDID) 89 - 90 - auther, err := auth.NewAuth( 91 - 100_000, 92 - time.Hour*12, 93 - 5, 94 - serviceWebDID, //+"#bsky_appview", 95 - ) 96 - if err != nil { 97 - log.Fatalf("Failed to create Auth: %v", err) 98 - } 99 - 100 - router := router_raw.Group("/") 101 - router.Use(auther.AuthenticateGinRequestViaJWT) 102 - 103 - router_unsafe := router_raw.Group("/") 104 - router_unsafe.Use(auther.AuthenticateGinRequestViaJWTUnsafe) 105 - 106 - responsewow, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", "did:web:did12.whey.party", "self") 107 - if err != nil { 108 - log.Println(err) 109 - } 110 - 111 - log.Println(responsewow.Uri) 112 - 113 - var didtest *utils.DID 114 - didval, errdid := utils.NewDID("did:web:did12.whey.party") 115 - if errdid != nil { 116 - didtest = nil 117 - } else { 118 - didtest = &didval 119 - } 120 - profiletest, _, _ := appbskyactordefs.ProfileViewBasic(ctx, *didtest, sl, BSKYIMAGECDN_URL) 121 - 122 - log.Println(*profiletest.DisplayName) 123 - log.Println(*profiletest.Avatar) 124 - 125 - router.GET("/ws", func(c *gin.Context) { 126 - mailbox.HandleWS(c.Writer, c.Request) 127 - }) 128 - 129 - kv := store.NewKV() 130 - 131 - // sad attempt to get putpref working. tldr it wont work without a client fork 132 - // https://bsky.app/profile/did:web:did12.whey.party/post/3m75xtomd722n 133 - router.GET("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 134 - c.Status(200) 135 - }) 136 - router.PUT("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 137 - c.Status(200) 138 - }) 139 - router.POST("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 140 - c.Status(200) 141 - 142 - userDID := c.GetString("user_did") 143 - body, err := io.ReadAll(c.Request.Body) 144 - if err != nil { 145 - c.JSON(400, gin.H{"error": "invalid body"}) 146 - return 147 - } 148 - 149 - kv.Set(userDID, body) 150 - 151 - }) 152 - 153 - router.GET("/xrpc/app.bsky.actor.getPreferences", func(c *gin.Context) { 154 - userDID := c.GetString("user_did") 155 - val, ok := kv.Get(userDID) 156 - if !ok { 157 - c.JSON(200, gin.H{"preferences": []any{}}) 158 - return 159 - } 160 - 161 - c.Data(200, "application/json", val) 162 - 163 - }) 164 - 165 - bskyappdid, _ := utils.NewDID("did:plc:z72i7hdynmk6r22z27h6tvur") 166 - 167 - profiletest2, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, bskyappdid, sl, cs, BSKYIMAGECDN_URL) 168 - 169 - data, err := json.MarshalIndent(profiletest2, "", " ") 170 - if err != nil { 171 - panic(err) 172 - } 173 - fmt.Println(string(data)) 174 - 175 - router.GET("/xrpc/app.bsky.actor.getProfiles", 176 - func(c *gin.Context) { 177 - actors := c.QueryArray("actors") 178 - 179 - profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors)) 180 - 181 - for _, v := range actors { 182 - did, err := utils.NewDID(v) 183 - if err != nil { 184 - continue 185 - } 186 - profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL) 187 - profiles = append(profiles, profile) 188 - } 189 - 190 - c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{ 191 - Profiles: profiles, 192 - }) 193 - }) 194 - 195 - router.GET("/xrpc/app.bsky.actor.getProfile", 196 - func(c *gin.Context) { 197 - actor := c.Query("actor") 198 - did, err := utils.NewDID(actor) 199 - if err != nil { 200 - c.JSON(http.StatusBadRequest, nil) 201 - return 202 - } 203 - profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL) 204 - c.JSON(http.StatusOK, profile) 205 - }) 206 - 207 - // really bad actually 208 - router.GET("/xrpc/app.bsky.notification.listNotifications", 209 - func(c *gin.Context) { 210 - emptyarray := []*appbsky.NotificationListNotifications_Notification{} 211 - notifshim := &appbsky.NotificationListNotifications_Output{ 212 - Notifications: emptyarray, 213 - } 214 - c.JSON(http.StatusOK, notifshim) 215 - }) 216 - 217 - router.GET("/xrpc/app.bsky.labeler.getServices", 218 - func(c *gin.Context) { 219 - dids := c.QueryArray("dids") 220 - 221 - labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids)) 222 - //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids)) 223 - 224 - for _, v := range dids { 225 - did, err := utils.NewDID(v) 226 - if err != nil { 227 - continue 228 - } 229 - labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, BSKYIMAGECDN_URL) 230 - labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self") 231 - var labelerservice appbsky.LabelerService 232 - if labelerserviceresponse != nil { 233 - if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil { 234 - continue 235 - } 236 - } 237 - 238 - a := "account" 239 - b := "record" 240 - c := "chat" 241 - 242 - placeholderTypes := []*string{&a, &b, &c} 243 - 244 - labeler := &appbsky.LabelerGetServices_Output_Views_Elem{ 245 - LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{ 246 - // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"` 247 - LexiconTypeID: "app.bsky.labeler.defs#labelerView", 248 - // Cid string `json:"cid" cborgen:"cid"` 249 - Cid: *labelerserviceresponse.Cid, 250 - // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 251 - Creator: labelerprofile, 252 - // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 253 - IndexedAt: labelerservice.CreatedAt, 254 - // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 255 - Labels: nil, // seems to always be empty? 256 - // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 257 - LikeCount: nil, // placeholder sorry 258 - // Uri string `json:"uri" cborgen:"uri"` 259 - Uri: labelerserviceresponse.Uri, 260 - // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 261 - Viewer: nil, 262 - }, 263 - LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{ 264 - // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"` 265 - LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed", 266 - // Cid string `json:"cid" cborgen:"cid"` 267 - Cid: *labelerserviceresponse.Cid, 268 - // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 269 - Creator: labelerprofile, 270 - // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 271 - IndexedAt: labelerservice.CreatedAt, 272 - // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 273 - Labels: nil, // seems to always be empty? 274 - // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 275 - LikeCount: nil, // placeholder sorry 276 - // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"` 277 - Policies: labelerservice.Policies, 278 - // // 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. 279 - // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"` 280 - ReasonTypes: nil, //usually not even present 281 - // // 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. 282 - // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"` 283 - SubjectCollections: nil, //usually not even present 284 - // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on. 285 - // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"` 286 - SubjectTypes: placeholderTypes, 287 - // Uri string `json:"uri" cborgen:"uri"` 288 - Uri: labelerserviceresponse.Uri, 289 - // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 290 - Viewer: nil, 291 - }, 292 - } 293 - labelers = append(labelers, labeler) 294 - } 295 - 296 - c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{ 297 - Views: labelers, 298 - }) 299 - }) 300 - 301 - router.GET("/xrpc/app.bsky.feed.getFeedGenerators", 302 - func(c *gin.Context) { 303 - feeds := c.QueryArray("feeds") 304 - ctx := c.Request.Context() 305 - 306 - type result struct { 307 - view *appbsky.FeedDefs_GeneratorView 308 - } 309 - 310 - results := make([]result, len(feeds)) 311 - 312 - var wg sync.WaitGroup 313 - wg.Add(len(feeds)) 314 - 315 - for i, raw := range feeds { 316 - go func(i int, raw string) { 317 - defer wg.Done() 318 - 319 - aturi, err := syntax.ParseATURI(raw) 320 - if err != nil { 321 - return 322 - } 323 - 324 - did := aturi.Authority().String() 325 - collection := aturi.Collection().String() 326 - rkey := aturi.RecordKey().String() 327 - 328 - repoDID, err := utils.NewDID(did) 329 - if err != nil { 330 - return 331 - } 332 - 333 - // fetch profile and record in parallel too (optional) 334 - // but to keep it simple, do serial inside this goroutine 335 - profile, _, _ := appbskyactordefs.ProfileView(ctx, repoDID, sl, BSKYIMAGECDN_URL) 336 - 337 - rec, err := agnostic.RepoGetRecord(ctx, sl, "", collection, did, rkey) 338 - if err != nil || rec.Value == nil { 339 - return 340 - } 341 - 342 - var genRec appbsky.FeedGenerator 343 - if err := json.Unmarshal(*rec.Value, &genRec); err != nil { 344 - return 345 - } 346 - 347 - var avatar *string 348 - if genRec.Avatar != nil { 349 - u := utils.MakeImageCDN(repoDID, BSKYIMAGECDN_URL, "avatar", genRec.Avatar.Ref.String()) 350 - avatar = &u 351 - } 352 - 353 - results[i].view = &appbsky.FeedDefs_GeneratorView{ 354 - LexiconTypeID: "app.bsky.feed.defs#generatorView", 355 - AcceptsInteractions: genRec.AcceptsInteractions, 356 - Avatar: avatar, 357 - Cid: *rec.Cid, 358 - ContentMode: genRec.ContentMode, 359 - Creator: profile, 360 - Description: genRec.Description, 361 - DescriptionFacets: genRec.DescriptionFacets, 362 - Did: did, 363 - DisplayName: genRec.DisplayName, 364 - IndexedAt: genRec.CreatedAt, 365 - Uri: rec.Uri, 366 - } 367 - }(i, raw) 368 - } 369 - 370 - wg.Wait() 371 - 372 - // build final slice 373 - out := make([]*appbsky.FeedDefs_GeneratorView, 0, len(results)) 374 - for _, r := range results { 375 - if r.view != nil { 376 - out = append(out, r.view) 377 - } 378 - } 379 - 380 - c.JSON(http.StatusOK, &appbsky.FeedGetFeedGenerators_Output{ 381 - Feeds: out, 382 - }) 383 - }) 384 - 385 - router.GET("/xrpc/app.bsky.feed.getPosts", 386 - func(c *gin.Context) { 387 - rawdid := c.GetString("user_did") 388 - var viewer *utils.DID 389 - didval, errdid := utils.NewDID(rawdid) 390 - if errdid != nil { 391 - viewer = nil 392 - } else { 393 - viewer = &didval 394 - } 395 - postsreq := c.QueryArray("uris") 396 - ctx := c.Request.Context() 397 - 398 - type result struct { 399 - view *appbsky.FeedDefs_PostView 400 - } 401 - 402 - results := make([]result, len(postsreq)) 403 - 404 - var wg sync.WaitGroup 405 - wg.Add(len(postsreq)) 406 - 407 - for i, raw := range postsreq { 408 - go func(i int, raw string) { 409 - defer wg.Done() 410 - 411 - post, _, _ := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 412 - 413 - results[i].view = post 414 - }(i, raw) 415 - } 416 - 417 - wg.Wait() 418 - 419 - // build final slice 420 - out := make([]*appbsky.FeedDefs_PostView, 0, len(results)) 421 - for _, r := range results { 422 - if r.view != nil { 423 - out = append(out, r.view) 424 - } 425 - } 426 - 427 - c.JSON(http.StatusOK, &appbsky.FeedGetPosts_Output{ 428 - Posts: out, 429 - }) 430 - }) 431 - 432 - router_unsafe.GET("/xrpc/app.bsky.feed.getFeed", 433 - func(c *gin.Context) { 434 - ctx := c.Request.Context() 435 - 436 - rawdid := c.GetString("user_did") 437 - log.Println("getFeed router_unsafe user_did: " + rawdid) 438 - var viewer *utils.DID 439 - didval, errdid := utils.NewDID(rawdid) 440 - if errdid != nil { 441 - viewer = nil 442 - } else { 443 - viewer = &didval 444 - } 445 - 446 - feedGenAturiRaw := c.Query("feed") 447 - if feedGenAturiRaw == "" { 448 - c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 449 - return 450 - } 451 - 452 - feedGenAturi, err := syntax.ParseATURI(feedGenAturiRaw) 453 - if err != nil { 454 - return 455 - } 456 - 457 - feedGeneratorRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.generator", feedGenAturi.Authority().String(), feedGenAturi.RecordKey().String()) 458 - if err != nil { 459 - c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve feed generator record: %v", err)}) 460 - return 461 - } 462 - 463 - var feedGeneratorRecord appbsky.FeedGenerator 464 - if err := json.Unmarshal(*feedGeneratorRecordResponse.Value, &feedGeneratorRecord); err != nil { 465 - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse feed generator record JSON"}) 466 - return 467 - } 468 - 469 - feedGenDID := feedGeneratorRecord.Did 470 - 471 - didDoc, err := ResolveDID(feedGenDID) 472 - if err != nil { 473 - c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)}) 474 - return 475 - } 476 - 477 - if err != nil { 478 - c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)}) 479 - return 480 - } 481 - 482 - var targetEndpoint string 483 - for _, svc := range didDoc.Service { 484 - if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") { 485 - targetEndpoint = svc.ServiceEndpoint 486 - break 487 - } 488 - } 489 - if targetEndpoint == "" { 490 - c.JSON(http.StatusBadGateway, gin.H{"error": "Feed Generator service endpoint not found in DID document"}) 491 - return 492 - } 493 - upstreamURL := fmt.Sprintf("%s/xrpc/app.bsky.feed.getFeedSkeleton?%s", 494 - strings.TrimSuffix(targetEndpoint, "/"), 495 - c.Request.URL.RawQuery, 496 - ) 497 - req, err := http.NewRequestWithContext(ctx, "GET", upstreamURL, nil) 498 - if err != nil { 499 - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upstream request"}) 500 - return 501 - } 502 - headersToForward := []string{"Authorization", "Content-Type", "Accept", "User-Agent"} 503 - for _, k := range headersToForward { 504 - if v := c.GetHeader(k); v != "" { 505 - req.Header.Set(k, v) 506 - } 507 - } 508 - client := &http.Client{} 509 - resp, err := client.Do(req) 510 - if err != nil { 511 - c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Upstream request failed: %v", err)}) 512 - return 513 - } 514 - defer resp.Body.Close() 515 - 516 - bodyBytes, err := io.ReadAll(resp.Body) 517 - if err != nil { 518 - c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read upstream body"}) 519 - return 520 - } 521 - if resp.StatusCode != http.StatusOK { 522 - // Forward the upstream error raw 523 - c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), bodyBytes) 524 - return 525 - } 526 - 527 - var feekskeleton appbsky.FeedGetFeedSkeleton_Output 528 - if err := json.Unmarshal(bodyBytes, &feekskeleton); err != nil { 529 - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream JSON"}) 530 - return 531 - } 532 - 533 - skeletonposts := feekskeleton.Feed 534 - 535 - concurrentResults := MapConcurrent( 536 - ctx, 537 - skeletonposts, 538 - 20, 539 - func(ctx context.Context, raw *appbsky.FeedDefs_SkeletonFeedPost) (*appbsky.FeedDefs_FeedViewPost, error) { 540 - post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 541 - if err != nil { 542 - return nil, err 543 - } 544 - if post == nil { 545 - return nil, fmt.Errorf("post not found") 546 - } 547 - 548 - return &appbsky.FeedDefs_FeedViewPost{ 549 - // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"` 550 - // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 551 - Post: post, 552 - // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"` 553 - // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{ 554 - // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost 555 - // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{ 556 - // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"` 557 - // LexiconTypeID: "app.bsky.feed.defs#reasonRepost", 558 - // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"` 559 - // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 560 - // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 561 - // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"` 562 - // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost, 563 - // }, 564 - // // FeedDefs_ReasonPin *FeedDefs_ReasonPin 565 - // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{ 566 - // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"` 567 - // LexiconTypeID: "app.bsky.feed.defs#reasonPin", 568 - // }, 569 - // }, 570 - // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"` 571 - // // reqId: Unique identifier per request that may be passed back alongside interactions. 572 - // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"` 573 - }, nil 574 - }, 575 - ) 576 - 577 - // build final slice 578 - out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults)) 579 - for _, r := range concurrentResults { 580 - if r.Err == nil && r.Value != nil && r.Value.Post != nil { 581 - out = append(out, r.Value) 582 - } 583 - } 584 - 585 - c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{ 586 - Cursor: feekskeleton.Cursor, 587 - Feed: out, 588 - }) 589 - }) 590 - type GetPostThreadOtherV2_Output_WithOtherReplies struct { 591 - appbsky.UnspeccedGetPostThreadOtherV2_Output 592 - HasOtherReplies bool `json:"hasOtherReplies"` 593 - } 594 - router.GET("/xrpc/app.bsky.unspecced.getPostThreadV2", 595 - func(c *gin.Context) { 596 - ctx := c.Request.Context() 597 - 598 - rawdid := c.GetString("user_did") 599 - var viewer *utils.DID 600 - didval, errdid := utils.NewDID(rawdid) 601 - if errdid != nil { 602 - viewer = nil 603 - } else { 604 - viewer = &didval 605 - } 606 - 607 - threadAnchorURIraw := c.Query("anchor") 608 - if threadAnchorURIraw == "" { 609 - c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 610 - return 611 - } 612 - 613 - threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 614 - if err != nil { 615 - return 616 - } 617 - 618 - //var thread []*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem 619 - 620 - var skeletonposts []string 621 - skeletonposts = append(skeletonposts, threadAnchorURI.String()) 622 - 623 - emptystrarray := &[]string{} 624 - limit := 100 625 - 626 - // todo: theres a cursor!!! pagination please! 627 - // todo: also i doubt im gonna do proper threadding so make sure to remind me to do it properly thanks 628 - //rootReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, nil) 629 - parentReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, nil) 630 - 631 - for _, rec := range parentReplies.Records { 632 - recordaturi, err := syntax.ParseATURI("at://" + rec.Did + "/" + rec.Collection + "/" + rec.Rkey) 633 - if err != nil { 634 - continue 635 - } 636 - skeletonposts = append(skeletonposts, recordaturi.String()) 637 - } 638 - concurrentResults := MapConcurrent( 639 - ctx, 640 - skeletonposts, 641 - 20, 642 - func(ctx context.Context, raw string) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) { 643 - post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 3) 644 - if err != nil { 645 - return nil, err 646 - } 647 - if post == nil { 648 - return nil, fmt.Errorf("post not found") 649 - } 650 - 651 - depth := int64(1) 652 - if raw == threadAnchorURI.String() { 653 - depth = 0 654 - } 655 - 656 - return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{ 657 - // Depth int64 `json:"depth" cborgen:"depth"` 658 - Depth: depth, // todo: placeholder 659 - // Uri string `json:"uri" cborgen:"uri"` 660 - Uri: raw, 661 - // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"` 662 - Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{ 663 - // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost 664 - UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{ 665 - // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"` 666 - LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost", 667 - // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. 668 - // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"` 669 - HiddenByThreadgate: false, // todo: placeholder 670 - // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents. 671 - // MoreParents bool `json:"moreParents" cborgen:"moreParents"` 672 - MoreParents: false, // todo: placeholder 673 - // // 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. 674 - // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"` 675 - MoreReplies: 0, // todo: placeholder 676 - // // mutedByViewer: This is by an account muted by the viewer requesting it. 677 - // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"` 678 - MutedByViewer: false, // todo: placeholder 679 - // // 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. 680 - // OpThread bool `json:"opThread" cborgen:"opThread"` 681 - OpThread: false, // todo: placeholder 682 - // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 683 - Post: post, 684 - }, 685 - }, 686 - }, nil 687 - }, 688 - ) 689 - 690 - // build final slice 691 - out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults)) 692 - for _, r := range concurrentResults { 693 - if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil { 694 - out = append(out, r.Value) 695 - } 696 - } 697 - 698 - // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{ 699 - // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"` 700 - // Thread: out, 701 - // HasOtherReplies: false, 702 - // }) 703 - resp := &GetPostThreadOtherV2_Output_WithOtherReplies{ 704 - UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{ 705 - Thread: out, 706 - }, 707 - HasOtherReplies: false, 708 - } 709 - c.JSON(http.StatusOK, resp) 710 - }) 711 - 712 - // weird stuff 713 - yourJSONBytes, _ := os.ReadFile("./public/getConfig.json") 714 - router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) { 715 - c.DataFromReader(200, -1, "application/json", 716 - bytes.NewReader(yourJSONBytes), nil) 717 - }) 718 - 719 - router.GET("/", func(c *gin.Context) { 720 - log.Println("hello worldio !") 721 - clientUUID := sticket.GetUUIDFromRequest(c.Request) 722 - hasSticket := clientUUID != "" 723 - if hasSticket { 724 - go func(targetUUID string) { 725 - // simulated heavy processing 726 - time.Sleep(2 * time.Second) 727 - 728 - lateData := map[string]any{ 729 - "postId": 101, 730 - "newComments": []string{ 731 - "Wow great tutorial!", 732 - "I am stuck on step 1.", 733 - }, 734 - } 735 - 736 - success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 737 - if success { 738 - log.Println("Successfully sent late data via Sticket") 739 - } else { 740 - log.Println("Failed to send late data (client disconnected?)") 741 - } 742 - }(clientUUID) 743 - } 744 - }) 745 - router_raw.Run(":7152") 746 - } 747 - 748 - func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 749 - log.Println("hello worldio !") 750 - } 751 - 752 - type DidResponse struct { 753 - Context []string `json:"@context"` 754 - ID string `json:"id"` 755 - Service []did.Service `json:"service"` 756 - } 757 - 758 - /* 759 - { 760 - id: "#bsky_appview", 761 - type: "BskyAppView", 762 - serviceEndpoint: endpoint, 763 - }, 764 - */ 765 - func GetWellKnownDID(c *gin.Context) { 766 - // Use a custom struct to fix missing omitempty on did.Document 767 - serviceEndpoint := serviceWebHost 768 - serviceDID, err := did.ParseDID(serviceWebDID) 769 - if err != nil { 770 - log.Println(fmt.Errorf("error parsing serviceDID: %w", err)) 771 - return 772 - } 773 - serviceID, err := did.ParseDID("#bsky_appview") 774 - if err != nil { 775 - panic(err) 776 - } 777 - didDoc := did.Document{ 778 - Context: []string{did.CtxDIDv1}, 779 - ID: serviceDID, 780 - Service: []did.Service{ 781 - { 782 - ID: serviceID, 783 - Type: "BskyAppView", 784 - ServiceEndpoint: serviceEndpoint, 785 - }, 786 - }, 787 - } 788 - didResponse := DidResponse{ 789 - Context: didDoc.Context, 790 - ID: didDoc.ID.String(), 791 - Service: didDoc.Service, 792 - } 793 - c.JSON(http.StatusOK, didResponse) 794 - }
+49 -1
readme.md
··· 15 15 - `app.bsky.feed.getPosts` (post rendering is incomplete) 16 16 - `app.bsky.feed.getFeed` (post rendering is incomplete) 17 17 - `app.bsky.unspecced.getConfig` (placeholder) 18 - - `app.bsky.unspecced.getPostThreadV2` (thread rendering is incomplete) 18 + - `app.bsky.unspecced.getPostThreadV2` (mostly working! doesnt use prefered sort, not performant yet) 19 + 20 + > [!NOTE] 21 + > uh im not very confident with the current directory structure, so files and folders might move around 22 + 23 + ## Runnables 24 + run all of these using `go run .` inside the respective directories 25 + 26 + ### `/cmd/appview` 27 + the main entry point, the actual appview itself. the api server that implements app.bsky.* XRPC methods 28 + 29 + ### `/cmd/backstream` 30 + experimental backfiller that kinda (but not really) conforms to the jetstream event shape. designed to be ingested by consumers expecting jetstream 31 + 32 + ### `/cmd/aturilist` 33 + experimental listRecords replacement. is not backfilled. uses the official jetstream go client, which means it suffers from this [bug](https://github.com/bluesky-social/jetstream/pull/45) 34 + 35 + ## Packages 36 + 37 + ### `/auth` 38 + taken from [go-bsky-feed-generator](https://github.com/jazware/go-bsky-feed-generator) but modified a bit. 39 + 40 + handles all of the auth, modified to have a more lenient version to make `getFeed` work 41 + 42 + ### `/microcosm/*` 43 + microcosm api clients, implements constellation slingshot and spacedust 44 + 45 + slingshot's api client is compatible with `github.com/bluesky-social/indigo/*` stuff, like `agnostic.RepoGetRecord` and `util.LexClient` 46 + 47 + ### `/shims/*` 48 + most of Red Dwarf Server logic lives here. pulls data from upstream services like microcosm constellation and slingshot, transforms it, and spits out bsky api -like responses using the published app.bsky.* codegen from `github.com/bluesky-social/indigo/api/bsky` 49 + 50 + 51 + ### `/sticket` 52 + unused leftover sorry 53 + 54 + 55 + ### `/store` 56 + unused leftover sorry 57 + 58 + ## todo 59 + 60 + - clean up /cmd/appview/main.go , its a mess 61 + - appview-side query caches 62 + - notification service 63 + - bookmarks service 64 + - create aturilist service 65 + - make backstream usable 66 + - create jetrelay service
+87 -7
shims/lex/app/bsky/actor/defs/profileview.go
··· 6 6 7 7 "github.com/bluesky-social/indigo/api/agnostic" 8 8 appbsky "github.com/bluesky-social/indigo/api/bsky" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 9 10 "tangled.org/whey.party/red-dwarf-server/microcosm" 10 11 "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 11 12 "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 12 13 "tangled.org/whey.party/red-dwarf-server/shims/utils" 13 14 ) 14 15 15 - func ProfileViewBasic(ctx context.Context, did utils.DID, sl *microcosm.MicrocosmClient, imgcdn string) (*appbsky.ActorDefs_ProfileViewBasic, *appbsky.ActorProfile, error) { 16 - profileview, profile, err := ProfileView(ctx, did, sl, imgcdn) 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) 17 18 18 19 if err != nil { 19 20 return nil, nil, err ··· 38 39 }, profile, err 39 40 } 40 41 41 - func ProfileView(ctx context.Context, did utils.DID, sl *microcosm.MicrocosmClient, imgcdn string) (*appbsky.ActorDefs_ProfileView, *appbsky.ActorProfile, error) { 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) { 42 43 identity, err_i := slingshot.ResolveMiniDoc(ctx, sl, string(did)) 43 44 if err_i != nil { 44 45 identity = nil ··· 76 77 avatar = &url 77 78 } 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 + 79 133 return &appbsky.ActorDefs_ProfileView{ 80 - Associated: nil, 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 + }, 81 145 Avatar: avatar, 82 146 CreatedAt: profile.CreatedAt, 83 147 Debug: nil, ··· 90 154 Pronouns: nil, 91 155 Status: nil, 92 156 Verification: nil, 93 - Viewer: 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 + }, 94 174 }, &profile, nil 95 175 } 96 176 97 - func ProfileViewDetailed(ctx context.Context, did utils.DID, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string) (*appbsky.ActorDefs_ProfileViewDetailed, *appbsky.ActorProfile, error) { 98 - profileview, profile, err := ProfileView(ctx, did, sl, imgcdn) 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) 99 179 if err != nil { 100 180 return nil, nil, err 101 181 }
+84 -17
shims/lex/app/bsky/feed/defs/embed.go
··· 41 41 return embedImage, nil 42 42 } 43 43 if feedPost.Embed.EmbedVideo != nil { 44 - return nil, 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 45 48 //embedType = "EmbedVideo" 46 - return &appbsky.FeedDefs_PostView_Embed{ 47 - // EmbedImages_View *EmbedImages_View 48 - // EmbedVideo_View *EmbedVideo_View 49 - EmbedVideo_View: &appbsky.EmbedVideo_View{ 50 - // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.embed.video#view"` 51 - // Alt *string `json:"alt,omitempty" cborgen:"alt,omitempty"` 52 - // AspectRatio *EmbedDefs_AspectRatio `json:"aspectRatio,omitempty" cborgen:"aspectRatio,omitempty"` 53 - // Cid string `json:"cid" cborgen:"cid"` 54 - // Playlist string `json:"playlist" cborgen:"playlist"` 55 - // Thumbnail *string `json:"thumbnail,omitempty" cborgen:"thumbnail,omitempty"` 56 - }, 57 - // EmbedExternal_View *EmbedExternal_View 58 - // EmbedRecord_View *EmbedRecord_View 59 - // EmbedRecordWithMedia_View *EmbedRecordWithMedia_View 60 - }, nil 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 61 66 } 62 67 if feedPost.Embed.EmbedExternal != nil { 63 68 embedExternal := EmbedExternalViewExtractor(ctx, aturi, feedPost.Embed.EmbedExternal, sl, cs, imgcdn, viewer) ··· 156 161 } 157 162 } 158 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 + } 159 177 // // video extractor 160 178 // embedmediaview = &appbsky.EmbedRecordWithMedia_View_Media{ 161 179 // // EmbedImages_View *EmbedImages_View ··· 242 260 243 261 } 244 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 + 245 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 { 246 308 // todo: gif embeds needs special handling i think? maybe? 247 309 //return nil, nil ··· 291 353 292 354 if collection == "app.bsky.feed.post" { 293 355 //t.EmbedRecord_ViewRecord.LexiconTypeID = "app.bsky.embed.record#viewRecord" 294 - profileViewBasic, _, err := appbskyactordefs.ProfileViewBasic(ctx, utils.DID(aturi.Authority().String()), sl, imgcdn) 356 + profileViewBasic, _, err := appbskyactordefs.ProfileViewBasic(ctx, utils.DID(aturi.Authority().String()), sl, cs, imgcdn, viewer) 295 357 if err != nil { 296 358 log.Println("[EmbedRecord_View_Record] profileviewbasic failed") 297 359 return notFoundRecordEmbed(aturi.String()) ··· 326 388 } 327 389 if postView.Embed.EmbedVideo_View != nil { 328 390 //has = "video" 391 + embeds = []*appbsky.EmbedRecord_ViewRecord_Embeds_Elem{ 392 + { 393 + EmbedVideo_View: postView.Embed.EmbedVideo_View, 394 + }, 395 + } 329 396 } 330 397 if postView.Embed.EmbedExternal_View != nil { 331 398 embeds = []*appbsky.EmbedRecord_ViewRecord_Embeds_Elem{
+21 -1
shims/lex/app/bsky/feed/defs/postview.go
··· 49 49 } 50 50 //} 51 51 52 - profile, _, err := appbskyactordefs.ProfileViewBasic(ctx, repoDID, sl, imgcdn) 52 + profile, _, err := appbskyactordefs.ProfileViewBasic(ctx, repoDID, sl, cs, imgcdn, viewer) 53 53 if err != nil || profile == nil { 54 54 if profile == nil { 55 55 //log.Println("WHAT!! profile / author field is null?!?!?! whyyy") ··· 76 76 subj, ok := like[".subject.uri"] 77 77 if ok { 78 78 likeCount = int64(subj.Records) 79 + } else { 80 + likeCount = int64(0) 79 81 } 82 + } else { 83 + likeCount = int64(0) 80 84 } 81 85 } 82 86 if links != nil && ··· 86 90 subj, ok := like[".subject.uri"] 87 91 if ok { 88 92 repostCount = int64(subj.Records) 93 + } else { 94 + repostCount = int64(0) 89 95 } 96 + } else { 97 + repostCount = int64(0) 90 98 } 91 99 } 92 100 if links != nil && ··· 96 104 subj, ok := like[".reply.parent.uri"] 97 105 if ok { 98 106 replyCount = int64(subj.Records) 107 + } else { 108 + replyCount = int64(0) 99 109 } 110 + } else { 111 + replyCount = int64(0) 100 112 } 101 113 } 102 114 if links != nil && ··· 106 118 subj, ok := like[".embed.record.uri"] 107 119 if ok { 108 120 quoteCount_noEmbed = int64(subj.Records) 121 + } else { 122 + quoteCount_noEmbed = int64(0) 109 123 } 124 + } else { 125 + quoteCount_noEmbed = int64(0) 110 126 } 111 127 } 112 128 if links != nil && ··· 116 132 subj, ok := like[".embed.record.record.uri"] 117 133 if ok { 118 134 quoteCount_withEmbed = int64(subj.Records) 135 + } else { 136 + quoteCount_withEmbed = int64(0) 119 137 } 138 + } else { 139 + quoteCount_withEmbed = int64(0) 120 140 } 121 141 } 122 142 quoteCount = quoteCount_noEmbed + quoteCount_withEmbed
+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 + }
+5
shims/utils/utils.go
··· 39 39 func MakeImageCDN(did DID, imgcdn string, kind string, cid string) string { 40 40 return imgcdn + "/img/" + kind + "/plain/" + string(did) + "/" + cid + "@jpeg" 41 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 + }
-127
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) (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) 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 - }