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, 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 type result struct {
514 view *appbsky.FeedDefs_FeedViewPost
515 }
516
517 results := make([]result, len(skeletonposts))
518
519 var wg sync.WaitGroup
520 wg.Add(len(skeletonposts))
521
522 for i, raw := range skeletonposts {
523 go func(i int, raw appbsky.FeedDefs_SkeletonFeedPost) {
524 defer wg.Done()
525
526 post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, BSKYIMAGECDN_URL)
527 if err != nil || post == nil {
528 return
529 }
530
531 feedviewpost := &appbsky.FeedDefs_FeedViewPost{
532 // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"`
533 // Post *FeedDefs_PostView `json:"post" cborgen:"post"`
534 Post: post,
535 // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"`
536 // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{
537 // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost
538 // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{
539 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"`
540 // LexiconTypeID: "app.bsky.feed.defs#reasonRepost",
541 // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"`
542 // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"`
543 // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"`
544 // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"`
545 // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost,
546 // },
547 // // FeedDefs_ReasonPin *FeedDefs_ReasonPin
548 // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{
549 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"`
550 // LexiconTypeID: "app.bsky.feed.defs#reasonPin",
551 // },
552 // },
553 // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"`
554 // // reqId: Unique identifier per request that may be passed back alongside interactions.
555 // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"`
556
557 }
558
559 results[i].view = feedviewpost
560 }(i, *raw)
561 }
562
563 wg.Wait()
564
565 // build final slice
566 out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(results))
567 for _, r := range results {
568 if r.view != nil && r.view.Post != nil {
569 out = append(out, r.view)
570 }
571 }
572
573 c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{
574 Cursor: feekskeleton.Cursor,
575 Feed: out,
576 })
577 })
578
579 yourJSONBytes, _ := os.ReadFile("./public/getConfig.json")
580 router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) {
581 c.DataFromReader(200, -1, "application/json",
582 bytes.NewReader(yourJSONBytes), nil)
583 })
584
585 router.GET("/", func(c *gin.Context) {
586 log.Println("hello worldio !")
587 clientUUID := sticket.GetUUIDFromRequest(c.Request)
588 hasSticket := clientUUID != ""
589 if hasSticket {
590 go func(targetUUID string) {
591 // simulated heavy processing
592 time.Sleep(2 * time.Second)
593
594 lateData := map[string]any{
595 "postId": 101,
596 "newComments": []string{
597 "Wow great tutorial!",
598 "I am stuck on step 1.",
599 },
600 }
601
602 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData)
603 if success {
604 log.Println("Successfully sent late data via Sticket")
605 } else {
606 log.Println("Failed to send late data (client disconnected?)")
607 }
608 }(clientUUID)
609 }
610 })
611 r_unsafe.Run(":7152")
612}
613
614func getPostThreadV2(w http.ResponseWriter, r *http.Request) {
615 log.Println("hello worldio !")
616}
617
618type DidResponse struct {
619 Context []string `json:"@context"`
620 ID string `json:"id"`
621 Service []did.Service `json:"service"`
622}
623
624/*
625 {
626 id: "#bsky_appview",
627 type: "BskyAppView",
628 serviceEndpoint: endpoint,
629 },
630*/
631func GetWellKnownDID(c *gin.Context) {
632 // Use a custom struct to fix missing omitempty on did.Document
633 serviceEndpoint := serviceWebHost
634 serviceDID, err := did.ParseDID(serviceWebDID)
635 if err != nil {
636 log.Println(fmt.Errorf("error parsing serviceDID: %w", err))
637 return
638 }
639 serviceID, err := did.ParseDID("#bsky_appview")
640 if err != nil {
641 panic(err)
642 }
643 didDoc := did.Document{
644 Context: []string{did.CtxDIDv1},
645 ID: serviceDID,
646 Service: []did.Service{
647 {
648 ID: serviceID,
649 Type: "BskyAppView",
650 ServiceEndpoint: serviceEndpoint,
651 },
652 },
653 }
654 didResponse := DidResponse{
655 Context: didDoc.Context,
656 ID: didDoc.ID.String(),
657 Service: didDoc.Service,
658 }
659 c.JSON(http.StatusOK, didResponse)
660}