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, cs, BSKYIMAGECDN_URL, nil)
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, nil)
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 rawdid := c.GetString("user_did")
180 var viewer *utils.DID
181 didval, errdid := utils.NewDID(rawdid)
182 if errdid != nil {
183 viewer = nil
184 } else {
185 viewer = &didval
186 }
187
188 profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors))
189
190 for _, v := range actors {
191 did, err := utils.NewDID(v)
192 if err != nil {
193 continue
194 }
195 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer)
196 profiles = append(profiles, profile)
197 }
198
199 c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{
200 Profiles: profiles,
201 })
202 })
203
204 router.GET("/xrpc/app.bsky.actor.getProfile",
205 func(c *gin.Context) {
206 actor := c.Query("actor")
207 did, err := utils.NewDID(actor)
208 if err != nil {
209 c.JSON(http.StatusBadRequest, nil)
210 return
211 }
212 rawdid := c.GetString("user_did")
213 var viewer *utils.DID
214 didval, errdid := utils.NewDID(rawdid)
215 if errdid != nil {
216 viewer = nil
217 } else {
218 viewer = &didval
219 }
220 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer)
221 c.JSON(http.StatusOK, profile)
222 })
223
224 // really bad actually
225 router.GET("/xrpc/app.bsky.notification.listNotifications",
226 func(c *gin.Context) {
227 emptyarray := []*appbsky.NotificationListNotifications_Notification{}
228 notifshim := &appbsky.NotificationListNotifications_Output{
229 Notifications: emptyarray,
230 }
231 c.JSON(http.StatusOK, notifshim)
232 })
233
234 router.GET("/xrpc/app.bsky.labeler.getServices",
235 func(c *gin.Context) {
236 dids := c.QueryArray("dids")
237
238 labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids))
239 //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids))
240
241 for _, v := range dids {
242 did, err := utils.NewDID(v)
243 if err != nil {
244 continue
245 }
246 rawdid := c.GetString("user_did")
247 var viewer *utils.DID
248 didval, errdid := utils.NewDID(rawdid)
249 if errdid != nil {
250 viewer = nil
251 } else {
252 viewer = &didval
253 }
254 labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer)
255 labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self")
256 var labelerservice appbsky.LabelerService
257 if labelerserviceresponse != nil {
258 if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil {
259 continue
260 }
261 }
262
263 a := "account"
264 b := "record"
265 c := "chat"
266
267 placeholderTypes := []*string{&a, &b, &c}
268
269 labeler := &appbsky.LabelerGetServices_Output_Views_Elem{
270 LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{
271 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"`
272 LexiconTypeID: "app.bsky.labeler.defs#labelerView",
273 // Cid string `json:"cid" cborgen:"cid"`
274 Cid: *labelerserviceresponse.Cid,
275 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"`
276 Creator: labelerprofile,
277 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
278 IndexedAt: labelerservice.CreatedAt,
279 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"`
280 Labels: nil, // seems to always be empty?
281 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"`
282 LikeCount: nil, // placeholder sorry
283 // Uri string `json:"uri" cborgen:"uri"`
284 Uri: labelerserviceresponse.Uri,
285 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"`
286 Viewer: nil,
287 },
288 LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{
289 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"`
290 LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed",
291 // Cid string `json:"cid" cborgen:"cid"`
292 Cid: *labelerserviceresponse.Cid,
293 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"`
294 Creator: labelerprofile,
295 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
296 IndexedAt: labelerservice.CreatedAt,
297 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"`
298 Labels: nil, // seems to always be empty?
299 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"`
300 LikeCount: nil, // placeholder sorry
301 // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"`
302 Policies: labelerservice.Policies,
303 // // 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.
304 // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"`
305 ReasonTypes: nil, //usually not even present
306 // // 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.
307 // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"`
308 SubjectCollections: nil, //usually not even present
309 // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on.
310 // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"`
311 SubjectTypes: placeholderTypes,
312 // Uri string `json:"uri" cborgen:"uri"`
313 Uri: labelerserviceresponse.Uri,
314 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"`
315 Viewer: nil,
316 },
317 }
318 labelers = append(labelers, labeler)
319 }
320
321 c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{
322 Views: labelers,
323 })
324 })
325
326 router.GET("/xrpc/app.bsky.feed.getFeedGenerators",
327 func(c *gin.Context) {
328 feeds := c.QueryArray("feeds")
329 ctx := c.Request.Context()
330
331 type result struct {
332 view *appbsky.FeedDefs_GeneratorView
333 }
334
335 results := make([]result, len(feeds))
336
337 var wg sync.WaitGroup
338 wg.Add(len(feeds))
339
340 for i, raw := range feeds {
341 go func(i int, raw string) {
342 defer wg.Done()
343
344 aturi, err := syntax.ParseATURI(raw)
345 if err != nil {
346 return
347 }
348
349 did := aturi.Authority().String()
350 collection := aturi.Collection().String()
351 rkey := aturi.RecordKey().String()
352
353 repoDID, err := utils.NewDID(did)
354 if err != nil {
355 return
356 }
357 rawdid := c.GetString("user_did")
358 var viewer *utils.DID
359 didval, errdid := utils.NewDID(rawdid)
360 if errdid != nil {
361 viewer = nil
362 } else {
363 viewer = &didval
364 }
365
366 // fetch profile and record in parallel too (optional)
367 // but to keep it simple, do serial inside this goroutine
368 profile, _, _ := appbskyactordefs.ProfileView(ctx, repoDID, sl, cs, BSKYIMAGECDN_URL, viewer)
369
370 rec, err := agnostic.RepoGetRecord(ctx, sl, "", collection, did, rkey)
371 if err != nil || rec.Value == nil {
372 return
373 }
374
375 var genRec appbsky.FeedGenerator
376 if err := json.Unmarshal(*rec.Value, &genRec); err != nil {
377 return
378 }
379
380 var avatar *string
381 if genRec.Avatar != nil {
382 u := utils.MakeImageCDN(repoDID, BSKYIMAGECDN_URL, "avatar", genRec.Avatar.Ref.String())
383 avatar = &u
384 }
385
386 results[i].view = &appbsky.FeedDefs_GeneratorView{
387 LexiconTypeID: "app.bsky.feed.defs#generatorView",
388 AcceptsInteractions: genRec.AcceptsInteractions,
389 Avatar: avatar,
390 Cid: *rec.Cid,
391 ContentMode: genRec.ContentMode,
392 Creator: profile,
393 Description: genRec.Description,
394 DescriptionFacets: genRec.DescriptionFacets,
395 Did: did,
396 DisplayName: genRec.DisplayName,
397 IndexedAt: genRec.CreatedAt,
398 Uri: rec.Uri,
399 }
400 }(i, raw)
401 }
402
403 wg.Wait()
404
405 // build final slice
406 out := make([]*appbsky.FeedDefs_GeneratorView, 0, len(results))
407 for _, r := range results {
408 if r.view != nil {
409 out = append(out, r.view)
410 }
411 }
412
413 c.JSON(http.StatusOK, &appbsky.FeedGetFeedGenerators_Output{
414 Feeds: out,
415 })
416 })
417
418 router.GET("/xrpc/app.bsky.feed.getPosts",
419 func(c *gin.Context) {
420 rawdid := c.GetString("user_did")
421 var viewer *utils.DID
422 didval, errdid := utils.NewDID(rawdid)
423 if errdid != nil {
424 viewer = nil
425 } else {
426 viewer = &didval
427 }
428 postsreq := c.QueryArray("uris")
429 ctx := c.Request.Context()
430
431 type result struct {
432 view *appbsky.FeedDefs_PostView
433 }
434
435 results := make([]result, len(postsreq))
436
437 var wg sync.WaitGroup
438 wg.Add(len(postsreq))
439
440 for i, raw := range postsreq {
441 go func(i int, raw string) {
442 defer wg.Done()
443
444 post, _, _ := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2)
445
446 results[i].view = post
447 }(i, raw)
448 }
449
450 wg.Wait()
451
452 // build final slice
453 out := make([]*appbsky.FeedDefs_PostView, 0, len(results))
454 for _, r := range results {
455 if r.view != nil {
456 out = append(out, r.view)
457 }
458 }
459
460 c.JSON(http.StatusOK, &appbsky.FeedGetPosts_Output{
461 Posts: out,
462 })
463 })
464
465 router_unsafe.GET("/xrpc/app.bsky.feed.getFeed",
466 func(c *gin.Context) {
467 ctx := c.Request.Context()
468
469 rawdid := c.GetString("user_did")
470 log.Println("getFeed router_unsafe user_did: " + rawdid)
471 var viewer *utils.DID
472 didval, errdid := utils.NewDID(rawdid)
473 if errdid != nil {
474 viewer = nil
475 } else {
476 viewer = &didval
477 }
478
479 feedGenAturiRaw := c.Query("feed")
480 if feedGenAturiRaw == "" {
481 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"})
482 return
483 }
484
485 feedGenAturi, err := syntax.ParseATURI(feedGenAturiRaw)
486 if err != nil {
487 return
488 }
489
490 feedGeneratorRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.generator", feedGenAturi.Authority().String(), feedGenAturi.RecordKey().String())
491 if err != nil {
492 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve feed generator record: %v", err)})
493 return
494 }
495
496 var feedGeneratorRecord appbsky.FeedGenerator
497 if err := json.Unmarshal(*feedGeneratorRecordResponse.Value, &feedGeneratorRecord); err != nil {
498 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse feed generator record JSON"})
499 return
500 }
501
502 feedGenDID := feedGeneratorRecord.Did
503
504 didDoc, err := ResolveDID(feedGenDID)
505 if err != nil {
506 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)})
507 return
508 }
509
510 if err != nil {
511 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)})
512 return
513 }
514
515 var targetEndpoint string
516 for _, svc := range didDoc.Service {
517 if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") {
518 targetEndpoint = svc.ServiceEndpoint
519 break
520 }
521 }
522 if targetEndpoint == "" {
523 c.JSON(http.StatusBadGateway, gin.H{"error": "Feed Generator service endpoint not found in DID document"})
524 return
525 }
526 upstreamURL := fmt.Sprintf("%s/xrpc/app.bsky.feed.getFeedSkeleton?%s",
527 strings.TrimSuffix(targetEndpoint, "/"),
528 c.Request.URL.RawQuery,
529 )
530 req, err := http.NewRequestWithContext(ctx, "GET", upstreamURL, nil)
531 if err != nil {
532 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upstream request"})
533 return
534 }
535 headersToForward := []string{"Authorization", "Content-Type", "Accept", "User-Agent"}
536 for _, k := range headersToForward {
537 if v := c.GetHeader(k); v != "" {
538 req.Header.Set(k, v)
539 }
540 }
541 client := &http.Client{}
542 resp, err := client.Do(req)
543 if err != nil {
544 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Upstream request failed: %v", err)})
545 return
546 }
547 defer resp.Body.Close()
548
549 bodyBytes, err := io.ReadAll(resp.Body)
550 if err != nil {
551 c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read upstream body"})
552 return
553 }
554 if resp.StatusCode != http.StatusOK {
555 // Forward the upstream error raw
556 c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), bodyBytes)
557 return
558 }
559
560 var feekskeleton appbsky.FeedGetFeedSkeleton_Output
561 if err := json.Unmarshal(bodyBytes, &feekskeleton); err != nil {
562 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream JSON"})
563 return
564 }
565
566 skeletonposts := feekskeleton.Feed
567
568 concurrentResults := MapConcurrent(
569 ctx,
570 skeletonposts,
571 20,
572 func(ctx context.Context, raw *appbsky.FeedDefs_SkeletonFeedPost) (*appbsky.FeedDefs_FeedViewPost, error) {
573 post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, cs, BSKYIMAGECDN_URL, viewer, 2)
574 if err != nil {
575 return nil, err
576 }
577 if post == nil {
578 return nil, fmt.Errorf("post not found")
579 }
580
581 return &appbsky.FeedDefs_FeedViewPost{
582 // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"`
583 // Post *FeedDefs_PostView `json:"post" cborgen:"post"`
584 Post: post,
585 // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"`
586 // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{
587 // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost
588 // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{
589 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"`
590 // LexiconTypeID: "app.bsky.feed.defs#reasonRepost",
591 // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"`
592 // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"`
593 // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
594 // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"`
595 // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost,
596 // },
597 // // FeedDefs_ReasonPin *FeedDefs_ReasonPin
598 // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{
599 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"`
600 // LexiconTypeID: "app.bsky.feed.defs#reasonPin",
601 // },
602 // },
603 // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"`
604 // // reqId: Unique identifier per request that may be passed back alongside interactions.
605 // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"`
606 }, nil
607 },
608 )
609
610 // build final slice
611 out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults))
612 for _, r := range concurrentResults {
613 if r.Err == nil && r.Value != nil && r.Value.Post != nil {
614 out = append(out, r.Value)
615 }
616 }
617
618 c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{
619 Cursor: feekskeleton.Cursor,
620 Feed: out,
621 })
622 })
623 type GetPostThreadOtherV2_Output_WithOtherReplies struct {
624 appbsky.UnspeccedGetPostThreadOtherV2_Output
625 HasOtherReplies bool `json:"hasOtherReplies"`
626 }
627 router.GET("/xrpc/app.bsky.unspecced.getPostThreadV2",
628 func(c *gin.Context) {
629 ctx := c.Request.Context()
630
631 rawdid := c.GetString("user_did")
632 var viewer *utils.DID
633 didval, errdid := utils.NewDID(rawdid)
634 if errdid != nil {
635 viewer = nil
636 } else {
637 viewer = &didval
638 }
639
640 threadAnchorURIraw := c.Query("anchor")
641 if threadAnchorURIraw == "" {
642 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"})
643 return
644 }
645
646 threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw)
647 if err != nil {
648 return
649 }
650
651 //var thread []*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem
652
653 var skeletonposts []string
654 skeletonposts = append(skeletonposts, threadAnchorURI.String())
655
656 emptystrarray := &[]string{}
657 limit := 100
658
659 // todo: theres a cursor!!! pagination please!
660 // todo: also i doubt im gonna do proper threadding so make sure to remind me to do it properly thanks
661 //rootReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, nil)
662 parentReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, nil)
663
664 for _, rec := range parentReplies.Records {
665 recordaturi, err := syntax.ParseATURI("at://" + rec.Did + "/" + rec.Collection + "/" + rec.Rkey)
666 if err != nil {
667 continue
668 }
669 skeletonposts = append(skeletonposts, recordaturi.String())
670 }
671 concurrentResults := MapConcurrent(
672 ctx,
673 skeletonposts,
674 20,
675 func(ctx context.Context, raw string) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) {
676 post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 3)
677 if err != nil {
678 return nil, err
679 }
680 if post == nil {
681 return nil, fmt.Errorf("post not found")
682 }
683
684 depth := int64(1)
685 if raw == threadAnchorURI.String() {
686 depth = 0
687 }
688
689 return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{
690 // Depth int64 `json:"depth" cborgen:"depth"`
691 Depth: depth, // todo: placeholder
692 // Uri string `json:"uri" cborgen:"uri"`
693 Uri: raw,
694 // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"`
695 Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{
696 // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost
697 UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{
698 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"`
699 LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost",
700 // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread.
701 // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"`
702 HiddenByThreadgate: false, // todo: placeholder
703 // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents.
704 // MoreParents bool `json:"moreParents" cborgen:"moreParents"`
705 MoreParents: false, // todo: placeholder
706 // // 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.
707 // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"`
708 MoreReplies: 0, // todo: placeholder
709 // // mutedByViewer: This is by an account muted by the viewer requesting it.
710 // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"`
711 MutedByViewer: false, // todo: placeholder
712 // // 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.
713 // OpThread bool `json:"opThread" cborgen:"opThread"`
714 OpThread: false, // todo: placeholder
715 // Post *FeedDefs_PostView `json:"post" cborgen:"post"`
716 Post: post,
717 },
718 },
719 }, nil
720 },
721 )
722
723 // build final slice
724 out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults))
725 for _, r := range concurrentResults {
726 if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil {
727 out = append(out, r.Value)
728 }
729 }
730
731 // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{
732 // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"`
733 // Thread: out,
734 // HasOtherReplies: false,
735 // })
736 resp := &GetPostThreadOtherV2_Output_WithOtherReplies{
737 UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{
738 Thread: out,
739 },
740 HasOtherReplies: false,
741 }
742 c.JSON(http.StatusOK, resp)
743 })
744
745 // weird stuff
746 yourJSONBytes, _ := os.ReadFile("./public/getConfig.json")
747 router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) {
748 c.DataFromReader(200, -1, "application/json",
749 bytes.NewReader(yourJSONBytes), nil)
750 })
751
752 router.GET("/", func(c *gin.Context) {
753 log.Println("hello worldio !")
754 clientUUID := sticket.GetUUIDFromRequest(c.Request)
755 hasSticket := clientUUID != ""
756 if hasSticket {
757 go func(targetUUID string) {
758 // simulated heavy processing
759 time.Sleep(2 * time.Second)
760
761 lateData := map[string]any{
762 "postId": 101,
763 "newComments": []string{
764 "Wow great tutorial!",
765 "I am stuck on step 1.",
766 },
767 }
768
769 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData)
770 if success {
771 log.Println("Successfully sent late data via Sticket")
772 } else {
773 log.Println("Failed to send late data (client disconnected?)")
774 }
775 }(clientUUID)
776 }
777 })
778 router_raw.Run(":7152")
779}
780
781func getPostThreadV2(w http.ResponseWriter, r *http.Request) {
782 log.Println("hello worldio !")
783}
784
785type DidResponse struct {
786 Context []string `json:"@context"`
787 ID string `json:"id"`
788 Service []did.Service `json:"service"`
789}
790
791/*
792 {
793 id: "#bsky_appview",
794 type: "BskyAppView",
795 serviceEndpoint: endpoint,
796 },
797*/
798func GetWellKnownDID(c *gin.Context) {
799 // Use a custom struct to fix missing omitempty on did.Document
800 serviceEndpoint := serviceWebHost
801 serviceDID, err := did.ParseDID(serviceWebDID)
802 if err != nil {
803 log.Println(fmt.Errorf("error parsing serviceDID: %w", err))
804 return
805 }
806 serviceID, err := did.ParseDID("#bsky_appview")
807 if err != nil {
808 panic(err)
809 }
810 didDoc := did.Document{
811 Context: []string{did.CtxDIDv1},
812 ID: serviceDID,
813 Service: []did.Service{
814 {
815 ID: serviceID,
816 Type: "BskyAppView",
817 ServiceEndpoint: serviceEndpoint,
818 },
819 },
820 }
821 didResponse := DidResponse{
822 Context: didDoc.Context,
823 ID: didDoc.ID.String(),
824 Service: didDoc.Service,
825 }
826 c.JSON(http.StatusOK, didResponse)
827}