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}