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}