bluesky appview implementation using microcosm and other services
server.reddwarf.app
appview
bluesky
reddwarf
microcosm
1package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "flag"
8 "fmt"
9 "io"
10 "log"
11 "net/http"
12 "os"
13 "strings"
14 "sync"
15 "time"
16
17 did "github.com/whyrusleeping/go-did"
18 "tangled.org/whey.party/red-dwarf-server/auth"
19 "tangled.org/whey.party/red-dwarf-server/microcosm/constellation"
20 "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot"
21 appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs"
22 appbskyfeeddefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/feed/defs"
23 "tangled.org/whey.party/red-dwarf-server/shims/utils"
24 "tangled.org/whey.party/red-dwarf-server/sticket"
25 "tangled.org/whey.party/red-dwarf-server/store"
26
27 // "github.com/bluesky-social/indigo/atproto/atclient"
28 // comatproto "github.com/bluesky-social/indigo/api/atproto"
29 appbsky "github.com/bluesky-social/indigo/api/bsky"
30 "github.com/bluesky-social/indigo/atproto/syntax"
31
32 // "github.com/bluesky-social/indigo/atproto/atclient"
33 // "github.com/bluesky-social/indigo/atproto/identity"
34 // "github.com/bluesky-social/indigo/atproto/syntax"
35 "github.com/bluesky-social/indigo/api/agnostic"
36 "github.com/gin-contrib/cors"
37 "github.com/gin-gonic/gin"
38 // "github.com/bluesky-social/jetstream/pkg/models"
39)
40
41var (
42 JETSTREAM_URL string
43 SPACEDUST_URL string
44 SLINGSHOT_URL string
45 CONSTELLATION_URL string
46)
47
48func initURLs(prod bool) {
49 if !prod {
50 JETSTREAM_URL = "wss://jetstream.whey.party/subscribe"
51 SPACEDUST_URL = "wss://spacedust.whey.party/subscribe"
52 SLINGSHOT_URL = "https://slingshot.whey.party"
53 CONSTELLATION_URL = "https://constellation.whey.party"
54 } else {
55 JETSTREAM_URL = "ws://localhost:6008/subscribe"
56 SPACEDUST_URL = "ws://localhost:9998/subscribe"
57 SLINGSHOT_URL = "http://localhost:7729"
58 CONSTELLATION_URL = "http://localhost:7728"
59 }
60}
61
62const (
63 BSKYIMAGECDN_URL = "https://cdn.bsky.app"
64 BSKYVIDEOCDN_URL = "https://video.bsky.app"
65 serviceWebDID = "did:web:server.reddwarf.app"
66 serviceWebHost = "https://server.reddwarf.app"
67)
68
69func main() {
70 log.Println("red-dwarf-server started")
71 prod := flag.Bool("prod", false, "use production URLs instead of localhost")
72 flag.Parse()
73
74 initURLs(*prod)
75
76 ctx := context.Background()
77 mailbox := sticket.New()
78 sl := slingshot.NewSlingshot(SLINGSHOT_URL)
79 cs := constellation.NewConstellation(CONSTELLATION_URL)
80 // spacedust is type definitions only
81 // jetstream types is probably available from jetstream/pkg/models
82
83 router_raw := gin.New()
84 router_raw.Use(gin.Logger())
85 router_raw.Use(gin.Recovery())
86 router_raw.Use(cors.Default())
87
88 router_raw.GET("/.well-known/did.json", GetWellKnownDID)
89
90 auther, err := auth.NewAuth(
91 100_000,
92 time.Hour*12,
93 5,
94 serviceWebDID, //+"#bsky_appview",
95 )
96 if err != nil {
97 log.Fatalf("Failed to create Auth: %v", err)
98 }
99
100 router := router_raw.Group("/")
101 router.Use(auther.AuthenticateGinRequestViaJWT)
102
103 router_unsafe := router_raw.Group("/")
104 router_unsafe.Use(auther.AuthenticateGinRequestViaJWTUnsafe)
105
106 responsewow, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", "did:web:did12.whey.party", "self")
107 if err != nil {
108 log.Println(err)
109 }
110
111 log.Println(responsewow.Uri)
112
113 var didtest *utils.DID
114 didval, errdid := utils.NewDID("did:web:did12.whey.party")
115 if errdid != nil {
116 didtest = nil
117 } else {
118 didtest = &didval
119 }
120 profiletest, _, _ := appbskyactordefs.ProfileViewBasic(ctx, *didtest, sl, BSKYIMAGECDN_URL)
121
122 log.Println(*profiletest.DisplayName)
123 log.Println(*profiletest.Avatar)
124
125 router.GET("/ws", func(c *gin.Context) {
126 mailbox.HandleWS(c.Writer, c.Request)
127 })
128
129 kv := store.NewKV()
130
131 // sad attempt to get putpref working. tldr it wont work without a client fork
132 // https://bsky.app/profile/did:web:did12.whey.party/post/3m75xtomd722n
133 router.GET("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) {
134 c.Status(200)
135 })
136 router.PUT("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) {
137 c.Status(200)
138 })
139 router.POST("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) {
140 c.Status(200)
141
142 userDID := c.GetString("user_did")
143 body, err := io.ReadAll(c.Request.Body)
144 if err != nil {
145 c.JSON(400, gin.H{"error": "invalid body"})
146 return
147 }
148
149 kv.Set(userDID, body)
150
151 })
152
153 router.GET("/xrpc/app.bsky.actor.getPreferences", func(c *gin.Context) {
154 userDID := c.GetString("user_did")
155 val, ok := kv.Get(userDID)
156 if !ok {
157 c.JSON(200, gin.H{"preferences": []any{}})
158 return
159 }
160
161 c.Data(200, "application/json", val)
162
163 })
164
165 bskyappdid, _ := utils.NewDID("did:plc:z72i7hdynmk6r22z27h6tvur")
166
167 profiletest2, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, bskyappdid, sl, cs, BSKYIMAGECDN_URL)
168
169 data, err := json.MarshalIndent(profiletest2, "", " ")
170 if err != nil {
171 panic(err)
172 }
173 fmt.Println(string(data))
174
175 router.GET("/xrpc/app.bsky.actor.getProfiles",
176 func(c *gin.Context) {
177 actors := c.QueryArray("actors")
178
179 profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors))
180
181 for _, v := range actors {
182 did, err := utils.NewDID(v)
183 if err != nil {
184 continue
185 }
186 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL)
187 profiles = append(profiles, profile)
188 }
189
190 c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{
191 Profiles: profiles,
192 })
193 })
194
195 router.GET("/xrpc/app.bsky.actor.getProfile",
196 func(c *gin.Context) {
197 actor := c.Query("actor")
198 did, err := utils.NewDID(actor)
199 if err != nil {
200 c.JSON(http.StatusBadRequest, nil)
201 return
202 }
203 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL)
204 c.JSON(http.StatusOK, profile)
205 })
206
207 // really bad actually
208 router.GET("/xrpc/app.bsky.notification.listNotifications",
209 func(c *gin.Context) {
210 emptyarray := []*appbsky.NotificationListNotifications_Notification{}
211 notifshim := &appbsky.NotificationListNotifications_Output{
212 Notifications: emptyarray,
213 }
214 c.JSON(http.StatusOK, notifshim)
215 })
216
217 router.GET("/xrpc/app.bsky.labeler.getServices",
218 func(c *gin.Context) {
219 dids := c.QueryArray("dids")
220
221 labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids))
222 //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids))
223
224 for _, v := range dids {
225 did, err := utils.NewDID(v)
226 if err != nil {
227 continue
228 }
229 labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, BSKYIMAGECDN_URL)
230 labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self")
231 var labelerservice appbsky.LabelerService
232 if labelerserviceresponse != nil {
233 if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil {
234 continue
235 }
236 }
237
238 a := "account"
239 b := "record"
240 c := "chat"
241
242 placeholderTypes := []*string{&a, &b, &c}
243
244 labeler := &appbsky.LabelerGetServices_Output_Views_Elem{
245 LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{
246 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"`
247 LexiconTypeID: "app.bsky.labeler.defs#labelerView",
248 // Cid string `json:"cid" cborgen:"cid"`
249 Cid: *labelerserviceresponse.Cid,
250 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"`
251 Creator: labelerprofile,
252 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
253 IndexedAt: labelerservice.CreatedAt,
254 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"`
255 Labels: nil, // seems to always be empty?
256 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"`
257 LikeCount: nil, // placeholder sorry
258 // Uri string `json:"uri" cborgen:"uri"`
259 Uri: labelerserviceresponse.Uri,
260 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"`
261 Viewer: nil,
262 },
263 LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{
264 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"`
265 LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed",
266 // Cid string `json:"cid" cborgen:"cid"`
267 Cid: *labelerserviceresponse.Cid,
268 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"`
269 Creator: labelerprofile,
270 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
271 IndexedAt: labelerservice.CreatedAt,
272 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"`
273 Labels: nil, // seems to always be empty?
274 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"`
275 LikeCount: nil, // placeholder sorry
276 // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"`
277 Policies: labelerservice.Policies,
278 // // reasonTypes: The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.
279 // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"`
280 ReasonTypes: nil, //usually not even present
281 // // subjectCollections: Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.
282 // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"`
283 SubjectCollections: nil, //usually not even present
284 // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on.
285 // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"`
286 SubjectTypes: placeholderTypes,
287 // Uri string `json:"uri" cborgen:"uri"`
288 Uri: labelerserviceresponse.Uri,
289 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"`
290 Viewer: nil,
291 },
292 }
293 labelers = append(labelers, labeler)
294 }
295
296 c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{
297 Views: labelers,
298 })
299 })
300
301 router.GET("/xrpc/app.bsky.feed.getFeedGenerators",
302 func(c *gin.Context) {
303 feeds := c.QueryArray("feeds")
304 ctx := c.Request.Context()
305
306 type result struct {
307 view *appbsky.FeedDefs_GeneratorView
308 }
309
310 results := make([]result, len(feeds))
311
312 var wg sync.WaitGroup
313 wg.Add(len(feeds))
314
315 for i, raw := range feeds {
316 go func(i int, raw string) {
317 defer wg.Done()
318
319 aturi, err := syntax.ParseATURI(raw)
320 if err != nil {
321 return
322 }
323
324 did := aturi.Authority().String()
325 collection := aturi.Collection().String()
326 rkey := aturi.RecordKey().String()
327
328 repoDID, err := utils.NewDID(did)
329 if err != nil {
330 return
331 }
332
333 // fetch profile and record in parallel too (optional)
334 // but to keep it simple, do serial inside this goroutine
335 profile, _, _ := appbskyactordefs.ProfileView(ctx, repoDID, sl, BSKYIMAGECDN_URL)
336
337 rec, err := agnostic.RepoGetRecord(ctx, sl, "", collection, did, rkey)
338 if err != nil || rec.Value == nil {
339 return
340 }
341
342 var genRec appbsky.FeedGenerator
343 if err := json.Unmarshal(*rec.Value, &genRec); err != nil {
344 return
345 }
346
347 var avatar *string
348 if genRec.Avatar != nil {
349 u := utils.MakeImageCDN(repoDID, BSKYIMAGECDN_URL, "avatar", genRec.Avatar.Ref.String())
350 avatar = &u
351 }
352
353 results[i].view = &appbsky.FeedDefs_GeneratorView{
354 LexiconTypeID: "app.bsky.feed.defs#generatorView",
355 AcceptsInteractions: genRec.AcceptsInteractions,
356 Avatar: avatar,
357 Cid: *rec.Cid,
358 ContentMode: genRec.ContentMode,
359 Creator: profile,
360 Description: genRec.Description,
361 DescriptionFacets: genRec.DescriptionFacets,
362 Did: did,
363 DisplayName: genRec.DisplayName,
364 IndexedAt: genRec.CreatedAt,
365 Uri: rec.Uri,
366 }
367 }(i, raw)
368 }
369
370 wg.Wait()
371
372 // build final slice
373 out := make([]*appbsky.FeedDefs_GeneratorView, 0, len(results))
374 for _, r := range results {
375 if r.view != nil {
376 out = append(out, r.view)
377 }
378 }
379
380 c.JSON(http.StatusOK, &appbsky.FeedGetFeedGenerators_Output{
381 Feeds: out,
382 })
383 })
384
385 router.GET("/xrpc/app.bsky.feed.getPosts",
386 func(c *gin.Context) {
387 rawdid := c.GetString("user_did")
388 var viewer *utils.DID
389 didval, errdid := utils.NewDID(rawdid)
390 if errdid != nil {
391 viewer = nil
392 } else {
393 viewer = &didval
394 }
395 postsreq := c.QueryArray("uris")
396 ctx := c.Request.Context()
397
398 type result struct {
399 view *appbsky.FeedDefs_PostView
400 }
401
402 results := make([]result, len(postsreq))
403
404 var wg sync.WaitGroup
405 wg.Add(len(postsreq))
406
407 for i, raw := range postsreq {
408 go func(i int, raw string) {
409 defer wg.Done()
410
411 post, _, _ := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2)
412
413 results[i].view = post
414 }(i, raw)
415 }
416
417 wg.Wait()
418
419 // build final slice
420 out := make([]*appbsky.FeedDefs_PostView, 0, len(results))
421 for _, r := range results {
422 if r.view != nil {
423 out = append(out, r.view)
424 }
425 }
426
427 c.JSON(http.StatusOK, &appbsky.FeedGetPosts_Output{
428 Posts: out,
429 })
430 })
431
432 router_unsafe.GET("/xrpc/app.bsky.feed.getFeed",
433 func(c *gin.Context) {
434 ctx := c.Request.Context()
435
436 rawdid := c.GetString("user_did")
437 log.Println("getFeed router_unsafe user_did: " + rawdid)
438 var viewer *utils.DID
439 didval, errdid := utils.NewDID(rawdid)
440 if errdid != nil {
441 viewer = nil
442 } else {
443 viewer = &didval
444 }
445
446 feedGenAturiRaw := c.Query("feed")
447 if feedGenAturiRaw == "" {
448 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"})
449 return
450 }
451
452 feedGenAturi, err := syntax.ParseATURI(feedGenAturiRaw)
453 if err != nil {
454 return
455 }
456
457 feedGeneratorRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.generator", feedGenAturi.Authority().String(), feedGenAturi.RecordKey().String())
458 if err != nil {
459 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve feed generator record: %v", err)})
460 return
461 }
462
463 var feedGeneratorRecord appbsky.FeedGenerator
464 if err := json.Unmarshal(*feedGeneratorRecordResponse.Value, &feedGeneratorRecord); err != nil {
465 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse feed generator record JSON"})
466 return
467 }
468
469 feedGenDID := feedGeneratorRecord.Did
470
471 didDoc, err := ResolveDID(feedGenDID)
472 if err != nil {
473 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)})
474 return
475 }
476
477 if err != nil {
478 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)})
479 return
480 }
481
482 var targetEndpoint string
483 for _, svc := range didDoc.Service {
484 if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") {
485 targetEndpoint = svc.ServiceEndpoint
486 break
487 }
488 }
489 if targetEndpoint == "" {
490 c.JSON(http.StatusBadGateway, gin.H{"error": "Feed Generator service endpoint not found in DID document"})
491 return
492 }
493 upstreamURL := fmt.Sprintf("%s/xrpc/app.bsky.feed.getFeedSkeleton?%s",
494 strings.TrimSuffix(targetEndpoint, "/"),
495 c.Request.URL.RawQuery,
496 )
497 req, err := http.NewRequestWithContext(ctx, "GET", upstreamURL, nil)
498 if err != nil {
499 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upstream request"})
500 return
501 }
502 headersToForward := []string{"Authorization", "Content-Type", "Accept", "User-Agent"}
503 for _, k := range headersToForward {
504 if v := c.GetHeader(k); v != "" {
505 req.Header.Set(k, v)
506 }
507 }
508 client := &http.Client{}
509 resp, err := client.Do(req)
510 if err != nil {
511 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Upstream request failed: %v", err)})
512 return
513 }
514 defer resp.Body.Close()
515
516 bodyBytes, err := io.ReadAll(resp.Body)
517 if err != nil {
518 c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read upstream body"})
519 return
520 }
521 if resp.StatusCode != http.StatusOK {
522 // Forward the upstream error raw
523 c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), bodyBytes)
524 return
525 }
526
527 var feekskeleton appbsky.FeedGetFeedSkeleton_Output
528 if err := json.Unmarshal(bodyBytes, &feekskeleton); err != nil {
529 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream JSON"})
530 return
531 }
532
533 skeletonposts := feekskeleton.Feed
534
535 concurrentResults := MapConcurrent(
536 ctx,
537 skeletonposts,
538 20,
539 func(ctx context.Context, raw *appbsky.FeedDefs_SkeletonFeedPost) (*appbsky.FeedDefs_FeedViewPost, error) {
540 post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, cs, BSKYIMAGECDN_URL, viewer, 2)
541 if err != nil {
542 return nil, err
543 }
544 if post == nil {
545 return nil, fmt.Errorf("post not found")
546 }
547
548 return &appbsky.FeedDefs_FeedViewPost{
549 // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"`
550 // Post *FeedDefs_PostView `json:"post" cborgen:"post"`
551 Post: post,
552 // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"`
553 // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{
554 // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost
555 // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{
556 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"`
557 // LexiconTypeID: "app.bsky.feed.defs#reasonRepost",
558 // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"`
559 // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"`
560 // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
561 // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"`
562 // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost,
563 // },
564 // // FeedDefs_ReasonPin *FeedDefs_ReasonPin
565 // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{
566 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"`
567 // LexiconTypeID: "app.bsky.feed.defs#reasonPin",
568 // },
569 // },
570 // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"`
571 // // reqId: Unique identifier per request that may be passed back alongside interactions.
572 // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"`
573 }, nil
574 },
575 )
576
577 // build final slice
578 out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults))
579 for _, r := range concurrentResults {
580 if r.Err == nil && r.Value != nil && r.Value.Post != nil {
581 out = append(out, r.Value)
582 }
583 }
584
585 c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{
586 Cursor: feekskeleton.Cursor,
587 Feed: out,
588 })
589 })
590 type GetPostThreadOtherV2_Output_WithOtherReplies struct {
591 appbsky.UnspeccedGetPostThreadOtherV2_Output
592 HasOtherReplies bool `json:"hasOtherReplies"`
593 }
594 router.GET("/xrpc/app.bsky.unspecced.getPostThreadV2",
595 func(c *gin.Context) {
596 ctx := c.Request.Context()
597
598 rawdid := c.GetString("user_did")
599 var viewer *utils.DID
600 didval, errdid := utils.NewDID(rawdid)
601 if errdid != nil {
602 viewer = nil
603 } else {
604 viewer = &didval
605 }
606
607 threadAnchorURIraw := c.Query("anchor")
608 if threadAnchorURIraw == "" {
609 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"})
610 return
611 }
612
613 threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw)
614 if err != nil {
615 return
616 }
617
618 //var thread []*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem
619
620 var skeletonposts []string
621 skeletonposts = append(skeletonposts, threadAnchorURI.String())
622
623 emptystrarray := &[]string{}
624 limit := 100
625
626 // todo: theres a cursor!!! pagination please!
627 // todo: also i doubt im gonna do proper threadding so make sure to remind me to do it properly thanks
628 //rootReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, nil)
629 parentReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, nil)
630
631 for _, rec := range parentReplies.Records {
632 recordaturi, err := syntax.ParseATURI("at://" + rec.Did + "/" + rec.Collection + "/" + rec.Rkey)
633 if err != nil {
634 continue
635 }
636 skeletonposts = append(skeletonposts, recordaturi.String())
637 }
638 concurrentResults := MapConcurrent(
639 ctx,
640 skeletonposts,
641 20,
642 func(ctx context.Context, raw string) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) {
643 post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 3)
644 if err != nil {
645 return nil, err
646 }
647 if post == nil {
648 return nil, fmt.Errorf("post not found")
649 }
650
651 depth := int64(1)
652 if raw == threadAnchorURI.String() {
653 depth = 0
654 }
655
656 return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{
657 // Depth int64 `json:"depth" cborgen:"depth"`
658 Depth: depth, // todo: placeholder
659 // Uri string `json:"uri" cborgen:"uri"`
660 Uri: raw,
661 // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"`
662 Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{
663 // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost
664 UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{
665 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"`
666 LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost",
667 // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread.
668 // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"`
669 HiddenByThreadgate: false, // todo: placeholder
670 // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents.
671 // MoreParents bool `json:"moreParents" cborgen:"moreParents"`
672 MoreParents: false, // todo: placeholder
673 // // moreReplies: This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate.
674 // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"`
675 MoreReplies: 0, // todo: placeholder
676 // // mutedByViewer: This is by an account muted by the viewer requesting it.
677 // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"`
678 MutedByViewer: false, // todo: placeholder
679 // // opThread: This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread.
680 // OpThread bool `json:"opThread" cborgen:"opThread"`
681 OpThread: false, // todo: placeholder
682 // Post *FeedDefs_PostView `json:"post" cborgen:"post"`
683 Post: post,
684 },
685 },
686 }, nil
687 },
688 )
689
690 // build final slice
691 out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults))
692 for _, r := range concurrentResults {
693 if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil {
694 out = append(out, r.Value)
695 }
696 }
697
698 // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{
699 // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"`
700 // Thread: out,
701 // HasOtherReplies: false,
702 // })
703 resp := &GetPostThreadOtherV2_Output_WithOtherReplies{
704 UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{
705 Thread: out,
706 },
707 HasOtherReplies: false,
708 }
709 c.JSON(http.StatusOK, resp)
710 })
711
712 // weird stuff
713 yourJSONBytes, _ := os.ReadFile("./public/getConfig.json")
714 router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) {
715 c.DataFromReader(200, -1, "application/json",
716 bytes.NewReader(yourJSONBytes), nil)
717 })
718
719 router.GET("/", func(c *gin.Context) {
720 log.Println("hello worldio !")
721 clientUUID := sticket.GetUUIDFromRequest(c.Request)
722 hasSticket := clientUUID != ""
723 if hasSticket {
724 go func(targetUUID string) {
725 // simulated heavy processing
726 time.Sleep(2 * time.Second)
727
728 lateData := map[string]any{
729 "postId": 101,
730 "newComments": []string{
731 "Wow great tutorial!",
732 "I am stuck on step 1.",
733 },
734 }
735
736 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData)
737 if success {
738 log.Println("Successfully sent late data via Sticket")
739 } else {
740 log.Println("Failed to send late data (client disconnected?)")
741 }
742 }(clientUUID)
743 }
744 })
745 router_raw.Run(":7152")
746}
747
748func getPostThreadV2(w http.ResponseWriter, r *http.Request) {
749 log.Println("hello worldio !")
750}
751
752type DidResponse struct {
753 Context []string `json:"@context"`
754 ID string `json:"id"`
755 Service []did.Service `json:"service"`
756}
757
758/*
759 {
760 id: "#bsky_appview",
761 type: "BskyAppView",
762 serviceEndpoint: endpoint,
763 },
764*/
765func GetWellKnownDID(c *gin.Context) {
766 // Use a custom struct to fix missing omitempty on did.Document
767 serviceEndpoint := serviceWebHost
768 serviceDID, err := did.ParseDID(serviceWebDID)
769 if err != nil {
770 log.Println(fmt.Errorf("error parsing serviceDID: %w", err))
771 return
772 }
773 serviceID, err := did.ParseDID("#bsky_appview")
774 if err != nil {
775 panic(err)
776 }
777 didDoc := did.Document{
778 Context: []string{did.CtxDIDv1},
779 ID: serviceDID,
780 Service: []did.Service{
781 {
782 ID: serviceID,
783 Type: "BskyAppView",
784 ServiceEndpoint: serviceEndpoint,
785 },
786 },
787 }
788 didResponse := DidResponse{
789 Context: didDoc.Context,
790 ID: didDoc.ID.String(),
791 Service: didDoc.Service,
792 }
793 c.JSON(http.StatusOK, didResponse)
794}