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 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
43var (
44 JETSTREAM_URL string
45 SPACEDUST_URL string
46 SLINGSHOT_URL string
47 CONSTELLATION_URL string
48 ATURILIST_URL string
49)
50
51func 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
67const (
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
74func 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
804This is an AT Protocol Application View (AppView) for any application that supports app.bsky.* xrpc methods.
805
806Most 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
816func getPostThreadV2(w http.ResponseWriter, r *http.Request) {
817 log.Println("hello worldio !")
818}
819
820type 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*/
833func 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}