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