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}