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 r_unsafe := gin.New()
84 r_unsafe.Use(gin.Logger())
85 r_unsafe.Use(gin.Recovery())
86 r_unsafe.Use(cors.Default())
87
88 r_unsafe.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 := r_unsafe.Group("/")
101 router.Use(auther.AuthenticateGinRequestViaJWT)
102 router.Use(auther.AuthenticateGinRequestViaJWT)
103
104 responsewow, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", "did:web:did12.whey.party", "self")
105 if err != nil {
106 log.Println(err)
107 }
108
109 log.Println(responsewow.Uri)
110
111 var didtest *utils.DID
112 didval, errdid := utils.NewDID("did:web:did12.whey.party")
113 if errdid != nil {
114 didtest = nil
115 } else {
116 didtest = &didval
117 }
118 profiletest, _, _ := appbskyactordefs.ProfileViewBasic(ctx, *didtest, sl, BSKYIMAGECDN_URL)
119
120 log.Println(*profiletest.DisplayName)
121 log.Println(*profiletest.Avatar)
122
123 router.GET("/ws", func(c *gin.Context) {
124 mailbox.HandleWS(c.Writer, c.Request)
125 })
126
127 kv := store.NewKV()
128
129 // sad attempt to get putpref working. tldr it wont work without a client fork
130 // https://bsky.app/profile/did:web:did12.whey.party/post/3m75xtomd722n
131 router.GET("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) {
132 c.Status(200)
133 })
134 router.PUT("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) {
135 c.Status(200)
136 })
137 router.POST("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) {
138 c.Status(200)
139
140 userDID := c.GetString("user_did")
141 body, err := io.ReadAll(c.Request.Body)
142 if err != nil {
143 c.JSON(400, gin.H{"error": "invalid body"})
144 return
145 }
146
147 kv.Set(userDID, body)
148
149 })
150
151 router.GET("/xrpc/app.bsky.actor.getPreferences", func(c *gin.Context) {
152 userDID := c.GetString("user_did")
153 val, ok := kv.Get(userDID)
154 if !ok {
155 c.JSON(200, gin.H{"preferences": []any{}})
156 return
157 }
158
159 c.Data(200, "application/json", val)
160
161 })
162
163 bskyappdid, _ := utils.NewDID("did:plc:z72i7hdynmk6r22z27h6tvur")
164
165 profiletest2, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, bskyappdid, sl, cs, BSKYIMAGECDN_URL)
166
167 data, err := json.MarshalIndent(profiletest2, "", " ")
168 if err != nil {
169 panic(err)
170 }
171 fmt.Println(string(data))
172
173 router.GET("/xrpc/app.bsky.actor.getProfiles",
174 func(c *gin.Context) {
175 actors := c.QueryArray("actors")
176
177 profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors))
178
179 for _, v := range actors {
180 did, err := utils.NewDID(v)
181 if err != nil {
182 continue
183 }
184 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL)
185 profiles = append(profiles, profile)
186 }
187
188 c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{
189 Profiles: profiles,
190 })
191 })
192
193 router.GET("/xrpc/app.bsky.actor.getProfile",
194 func(c *gin.Context) {
195 actor := c.Query("actor")
196 did, err := utils.NewDID(actor)
197 if err != nil {
198 c.JSON(http.StatusBadRequest, nil)
199 return
200 }
201 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL)
202 c.JSON(http.StatusOK, profile)
203 })
204
205 // really bad actually
206 router.GET("/xrpc/app.bsky.notification.listNotifications",
207 func(c *gin.Context) {
208 emptyarray := []*appbsky.NotificationListNotifications_Notification{}
209 notifshim := &appbsky.NotificationListNotifications_Output{
210 Notifications: emptyarray,
211 }
212 c.JSON(http.StatusOK, notifshim)
213 })
214
215 router.GET("/xrpc/app.bsky.labeler.getServices",
216 func(c *gin.Context) {
217 dids := c.QueryArray("dids")
218
219 labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids))
220 //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids))
221
222 for _, v := range dids {
223 did, err := utils.NewDID(v)
224 if err != nil {
225 continue
226 }
227 labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, BSKYIMAGECDN_URL)
228 labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self")
229 var labelerservice appbsky.LabelerService
230 if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil {
231 continue
232 }
233
234 a := "account"
235 b := "record"
236 c := "chat"
237
238 placeholderTypes := []*string{&a, &b, &c}
239
240 labeler := &appbsky.LabelerGetServices_Output_Views_Elem{
241 LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{
242 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"`
243 LexiconTypeID: "app.bsky.labeler.defs#labelerView",
244 // Cid string `json:"cid" cborgen:"cid"`
245 Cid: *labelerserviceresponse.Cid,
246 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"`
247 Creator: labelerprofile,
248 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
249 IndexedAt: labelerservice.CreatedAt,
250 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"`
251 Labels: nil, // seems to always be empty?
252 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"`
253 LikeCount: nil, // placeholder sorry
254 // Uri string `json:"uri" cborgen:"uri"`
255 Uri: labelerserviceresponse.Uri,
256 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"`
257 Viewer: nil,
258 },
259 LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{
260 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"`
261 LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed",
262 // Cid string `json:"cid" cborgen:"cid"`
263 Cid: *labelerserviceresponse.Cid,
264 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"`
265 Creator: labelerprofile,
266 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
267 IndexedAt: labelerservice.CreatedAt,
268 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"`
269 Labels: nil, // seems to always be empty?
270 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"`
271 LikeCount: nil, // placeholder sorry
272 // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"`
273 Policies: labelerservice.Policies,
274 // // 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.
275 // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"`
276 ReasonTypes: nil, //usually not even present
277 // // 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.
278 // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"`
279 SubjectCollections: nil, //usually not even present
280 // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on.
281 // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"`
282 SubjectTypes: placeholderTypes,
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 }
289 labelers = append(labelers, labeler)
290 }
291
292 c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{
293 Views: labelers,
294 })
295 })
296
297 router.GET("/xrpc/app.bsky.feed.getFeedGenerators",
298 func(c *gin.Context) {
299 feeds := c.QueryArray("feeds")
300 ctx := c.Request.Context()
301
302 type result struct {
303 view *appbsky.FeedDefs_GeneratorView
304 }
305
306 results := make([]result, len(feeds))
307
308 var wg sync.WaitGroup
309 wg.Add(len(feeds))
310
311 for i, raw := range feeds {
312 go func(i int, raw string) {
313 defer wg.Done()
314
315 aturi, err := syntax.ParseATURI(raw)
316 if err != nil {
317 return
318 }
319
320 did := aturi.Authority().String()
321 collection := aturi.Collection().String()
322 rkey := aturi.RecordKey().String()
323
324 repoDID, err := utils.NewDID(did)
325 if err != nil {
326 return
327 }
328
329 // fetch profile and record in parallel too (optional)
330 // but to keep it simple, do serial inside this goroutine
331 profile, _, _ := appbskyactordefs.ProfileView(ctx, repoDID, sl, BSKYIMAGECDN_URL)
332
333 rec, err := agnostic.RepoGetRecord(ctx, sl, "", collection, did, rkey)
334 if err != nil || rec.Value == nil {
335 return
336 }
337
338 var genRec appbsky.FeedGenerator
339 if err := json.Unmarshal(*rec.Value, &genRec); err != nil {
340 return
341 }
342
343 var avatar *string
344 if genRec.Avatar != nil {
345 u := utils.MakeImageCDN(repoDID, BSKYIMAGECDN_URL, "avatar", genRec.Avatar.Ref.String())
346 avatar = &u
347 }
348
349 results[i].view = &appbsky.FeedDefs_GeneratorView{
350 LexiconTypeID: "app.bsky.feed.defs#generatorView",
351 AcceptsInteractions: genRec.AcceptsInteractions,
352 Avatar: avatar,
353 Cid: *rec.Cid,
354 ContentMode: genRec.ContentMode,
355 Creator: profile,
356 Description: genRec.Description,
357 DescriptionFacets: genRec.DescriptionFacets,
358 Did: did,
359 DisplayName: genRec.DisplayName,
360 IndexedAt: genRec.CreatedAt,
361 Uri: rec.Uri,
362 }
363 }(i, raw)
364 }
365
366 wg.Wait()
367
368 // build final slice
369 out := make([]*appbsky.FeedDefs_GeneratorView, 0, len(results))
370 for _, r := range results {
371 if r.view != nil {
372 out = append(out, r.view)
373 }
374 }
375
376 c.JSON(http.StatusOK, &appbsky.FeedGetFeedGenerators_Output{
377 Feeds: out,
378 })
379 })
380
381 router.GET("/xrpc/app.bsky.feed.getPosts",
382 func(c *gin.Context) {
383 postsreq := c.QueryArray("uris")
384 ctx := c.Request.Context()
385
386 type result struct {
387 view *appbsky.FeedDefs_PostView
388 }
389
390 results := make([]result, len(postsreq))
391
392 var wg sync.WaitGroup
393 wg.Add(len(postsreq))
394
395 for i, raw := range postsreq {
396 go func(i int, raw string) {
397 defer wg.Done()
398
399 post, _, _ := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL)
400
401 results[i].view = post
402 }(i, raw)
403 }
404
405 wg.Wait()
406
407 // build final slice
408 out := make([]*appbsky.FeedDefs_PostView, 0, len(results))
409 for _, r := range results {
410 if r.view != nil {
411 out = append(out, r.view)
412 }
413 }
414
415 c.JSON(http.StatusOK, &appbsky.FeedGetPosts_Output{
416 Posts: out,
417 })
418 })
419
420 r_unsafe.GET("/xrpc/app.bsky.feed.getFeed",
421 func(c *gin.Context) {
422 ctx := c.Request.Context()
423
424 feedGenAturiRaw := c.Query("feed")
425 if feedGenAturiRaw == "" {
426 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"})
427 return
428 }
429
430 feedGenAturi, err := syntax.ParseATURI(feedGenAturiRaw)
431 if err != nil {
432 return
433 }
434
435 feedGeneratorRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.generator", feedGenAturi.Authority().String(), feedGenAturi.RecordKey().String())
436 if err != nil {
437 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve feed generator record: %v", err)})
438 return
439 }
440
441 var feedGeneratorRecord appbsky.FeedGenerator
442 if err := json.Unmarshal(*feedGeneratorRecordResponse.Value, &feedGeneratorRecord); err != nil {
443 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse feed generator record JSON"})
444 return
445 }
446
447 feedGenDID := feedGeneratorRecord.Did
448
449 didDoc, err := ResolveDID(feedGenDID)
450 if err != nil {
451 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)})
452 return
453 }
454
455 if err != nil {
456 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)})
457 return
458 }
459
460 var targetEndpoint string
461 for _, svc := range didDoc.Service {
462 if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") {
463 targetEndpoint = svc.ServiceEndpoint
464 break
465 }
466 }
467 if targetEndpoint == "" {
468 c.JSON(http.StatusBadGateway, gin.H{"error": "Feed Generator service endpoint not found in DID document"})
469 return
470 }
471 upstreamURL := fmt.Sprintf("%s/xrpc/app.bsky.feed.getFeedSkeleton?%s",
472 strings.TrimSuffix(targetEndpoint, "/"),
473 c.Request.URL.RawQuery,
474 )
475 req, err := http.NewRequestWithContext(ctx, "GET", upstreamURL, nil)
476 if err != nil {
477 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upstream request"})
478 return
479 }
480 headersToForward := []string{"Authorization", "Content-Type", "Accept", "User-Agent"}
481 for _, k := range headersToForward {
482 if v := c.GetHeader(k); v != "" {
483 req.Header.Set(k, v)
484 }
485 }
486 client := &http.Client{}
487 resp, err := client.Do(req)
488 if err != nil {
489 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Upstream request failed: %v", err)})
490 return
491 }
492 defer resp.Body.Close()
493
494 bodyBytes, err := io.ReadAll(resp.Body)
495 if err != nil {
496 c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read upstream body"})
497 return
498 }
499 if resp.StatusCode != http.StatusOK {
500 // Forward the upstream error raw
501 c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), bodyBytes)
502 return
503 }
504
505 var feekskeleton appbsky.FeedGetFeedSkeleton_Output
506 if err := json.Unmarshal(bodyBytes, &feekskeleton); err != nil {
507 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream JSON"})
508 return
509 }
510
511 skeletonposts := feekskeleton.Feed
512
513 concurrentResults := MapConcurrent(
514 ctx,
515 skeletonposts,
516 20,
517 func(ctx context.Context, raw *appbsky.FeedDefs_SkeletonFeedPost) (*appbsky.FeedDefs_FeedViewPost, error) {
518 post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, cs, BSKYIMAGECDN_URL)
519 if err != nil {
520 return nil, err
521 }
522 if post == nil {
523 return nil, fmt.Errorf("post not found")
524 }
525
526 return &appbsky.FeedDefs_FeedViewPost{
527 // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"`
528 // Post *FeedDefs_PostView `json:"post" cborgen:"post"`
529 Post: post,
530 // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"`
531 // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{
532 // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost
533 // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{
534 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"`
535 // LexiconTypeID: "app.bsky.feed.defs#reasonRepost",
536 // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"`
537 // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"`
538 // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
539 // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"`
540 // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost,
541 // },
542 // // FeedDefs_ReasonPin *FeedDefs_ReasonPin
543 // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{
544 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"`
545 // LexiconTypeID: "app.bsky.feed.defs#reasonPin",
546 // },
547 // },
548 // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"`
549 // // reqId: Unique identifier per request that may be passed back alongside interactions.
550 // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"`
551 }, nil
552 },
553 )
554
555 // build final slice
556 out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults))
557 for _, r := range concurrentResults {
558 if r.Err == nil && r.Value != nil && r.Value.Post != nil {
559 out = append(out, r.Value)
560 }
561 }
562
563 c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{
564 Cursor: feekskeleton.Cursor,
565 Feed: out,
566 })
567 })
568
569 yourJSONBytes, _ := os.ReadFile("./public/getConfig.json")
570 router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) {
571 c.DataFromReader(200, -1, "application/json",
572 bytes.NewReader(yourJSONBytes), nil)
573 })
574
575 router.GET("/", func(c *gin.Context) {
576 log.Println("hello worldio !")
577 clientUUID := sticket.GetUUIDFromRequest(c.Request)
578 hasSticket := clientUUID != ""
579 if hasSticket {
580 go func(targetUUID string) {
581 // simulated heavy processing
582 time.Sleep(2 * time.Second)
583
584 lateData := map[string]any{
585 "postId": 101,
586 "newComments": []string{
587 "Wow great tutorial!",
588 "I am stuck on step 1.",
589 },
590 }
591
592 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData)
593 if success {
594 log.Println("Successfully sent late data via Sticket")
595 } else {
596 log.Println("Failed to send late data (client disconnected?)")
597 }
598 }(clientUUID)
599 }
600 })
601 r_unsafe.Run(":7152")
602}
603
604func getPostThreadV2(w http.ResponseWriter, r *http.Request) {
605 log.Println("hello worldio !")
606}
607
608type DidResponse struct {
609 Context []string `json:"@context"`
610 ID string `json:"id"`
611 Service []did.Service `json:"service"`
612}
613
614/*
615 {
616 id: "#bsky_appview",
617 type: "BskyAppView",
618 serviceEndpoint: endpoint,
619 },
620*/
621func GetWellKnownDID(c *gin.Context) {
622 // Use a custom struct to fix missing omitempty on did.Document
623 serviceEndpoint := serviceWebHost
624 serviceDID, err := did.ParseDID(serviceWebDID)
625 if err != nil {
626 log.Println(fmt.Errorf("error parsing serviceDID: %w", err))
627 return
628 }
629 serviceID, err := did.ParseDID("#bsky_appview")
630 if err != nil {
631 panic(err)
632 }
633 didDoc := did.Document{
634 Context: []string{did.CtxDIDv1},
635 ID: serviceDID,
636 Service: []did.Service{
637 {
638 ID: serviceID,
639 Type: "BskyAppView",
640 ServiceEndpoint: serviceEndpoint,
641 },
642 },
643 }
644 didResponse := DidResponse{
645 Context: didDoc.Context,
646 ID: didDoc.ID.String(),
647 Service: didDoc.Service,
648 }
649 c.JSON(http.StatusOK, didResponse)
650}