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