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, cs, BSKYIMAGECDN_URL, nil) 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, nil) 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 rawdid := c.GetString("user_did") 180 var viewer *utils.DID 181 didval, errdid := utils.NewDID(rawdid) 182 if errdid != nil { 183 viewer = nil 184 } else { 185 viewer = &didval 186 } 187 188 profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors)) 189 190 for _, v := range actors { 191 did, err := utils.NewDID(v) 192 if err != nil { 193 continue 194 } 195 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 196 profiles = append(profiles, profile) 197 } 198 199 c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{ 200 Profiles: profiles, 201 }) 202 }) 203 204 router.GET("/xrpc/app.bsky.actor.getProfile", 205 func(c *gin.Context) { 206 actor := c.Query("actor") 207 did, err := utils.NewDID(actor) 208 if err != nil { 209 c.JSON(http.StatusBadRequest, nil) 210 return 211 } 212 rawdid := c.GetString("user_did") 213 var viewer *utils.DID 214 didval, errdid := utils.NewDID(rawdid) 215 if errdid != nil { 216 viewer = nil 217 } else { 218 viewer = &didval 219 } 220 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 221 c.JSON(http.StatusOK, profile) 222 }) 223 224 // really bad actually 225 router.GET("/xrpc/app.bsky.notification.listNotifications", 226 func(c *gin.Context) { 227 emptyarray := []*appbsky.NotificationListNotifications_Notification{} 228 notifshim := &appbsky.NotificationListNotifications_Output{ 229 Notifications: emptyarray, 230 } 231 c.JSON(http.StatusOK, notifshim) 232 }) 233 234 router.GET("/xrpc/app.bsky.labeler.getServices", 235 func(c *gin.Context) { 236 dids := c.QueryArray("dids") 237 238 labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids)) 239 //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids)) 240 241 for _, v := range dids { 242 did, err := utils.NewDID(v) 243 if err != nil { 244 continue 245 } 246 rawdid := c.GetString("user_did") 247 var viewer *utils.DID 248 didval, errdid := utils.NewDID(rawdid) 249 if errdid != nil { 250 viewer = nil 251 } else { 252 viewer = &didval 253 } 254 labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 255 labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self") 256 var labelerservice appbsky.LabelerService 257 if labelerserviceresponse != nil { 258 if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil { 259 continue 260 } 261 } 262 263 a := "account" 264 b := "record" 265 c := "chat" 266 267 placeholderTypes := []*string{&a, &b, &c} 268 269 labeler := &appbsky.LabelerGetServices_Output_Views_Elem{ 270 LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{ 271 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"` 272 LexiconTypeID: "app.bsky.labeler.defs#labelerView", 273 // Cid string `json:"cid" cborgen:"cid"` 274 Cid: *labelerserviceresponse.Cid, 275 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 276 Creator: labelerprofile, 277 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 278 IndexedAt: labelerservice.CreatedAt, 279 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 280 Labels: nil, // seems to always be empty? 281 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 282 LikeCount: nil, // placeholder sorry 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 LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{ 289 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"` 290 LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed", 291 // Cid string `json:"cid" cborgen:"cid"` 292 Cid: *labelerserviceresponse.Cid, 293 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 294 Creator: labelerprofile, 295 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 296 IndexedAt: labelerservice.CreatedAt, 297 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 298 Labels: nil, // seems to always be empty? 299 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 300 LikeCount: nil, // placeholder sorry 301 // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"` 302 Policies: labelerservice.Policies, 303 // // 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. 304 // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"` 305 ReasonTypes: nil, //usually not even present 306 // // 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. 307 // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"` 308 SubjectCollections: nil, //usually not even present 309 // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on. 310 // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"` 311 SubjectTypes: placeholderTypes, 312 // Uri string `json:"uri" cborgen:"uri"` 313 Uri: labelerserviceresponse.Uri, 314 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 315 Viewer: nil, 316 }, 317 } 318 labelers = append(labelers, labeler) 319 } 320 321 c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{ 322 Views: labelers, 323 }) 324 }) 325 326 router.GET("/xrpc/app.bsky.feed.getFeedGenerators", 327 func(c *gin.Context) { 328 feeds := c.QueryArray("feeds") 329 ctx := c.Request.Context() 330 331 type result struct { 332 view *appbsky.FeedDefs_GeneratorView 333 } 334 335 results := make([]result, len(feeds)) 336 337 var wg sync.WaitGroup 338 wg.Add(len(feeds)) 339 340 for i, raw := range feeds { 341 go func(i int, raw string) { 342 defer wg.Done() 343 344 aturi, err := syntax.ParseATURI(raw) 345 if err != nil { 346 return 347 } 348 349 did := aturi.Authority().String() 350 collection := aturi.Collection().String() 351 rkey := aturi.RecordKey().String() 352 353 repoDID, err := utils.NewDID(did) 354 if err != nil { 355 return 356 } 357 rawdid := c.GetString("user_did") 358 var viewer *utils.DID 359 didval, errdid := utils.NewDID(rawdid) 360 if errdid != nil { 361 viewer = nil 362 } else { 363 viewer = &didval 364 } 365 366 // fetch profile and record in parallel too (optional) 367 // but to keep it simple, do serial inside this goroutine 368 profile, _, _ := appbskyactordefs.ProfileView(ctx, repoDID, sl, cs, BSKYIMAGECDN_URL, viewer) 369 370 rec, err := agnostic.RepoGetRecord(ctx, sl, "", collection, did, rkey) 371 if err != nil || rec.Value == nil { 372 return 373 } 374 375 var genRec appbsky.FeedGenerator 376 if err := json.Unmarshal(*rec.Value, &genRec); err != nil { 377 return 378 } 379 380 var avatar *string 381 if genRec.Avatar != nil { 382 u := utils.MakeImageCDN(repoDID, BSKYIMAGECDN_URL, "avatar", genRec.Avatar.Ref.String()) 383 avatar = &u 384 } 385 386 results[i].view = &appbsky.FeedDefs_GeneratorView{ 387 LexiconTypeID: "app.bsky.feed.defs#generatorView", 388 AcceptsInteractions: genRec.AcceptsInteractions, 389 Avatar: avatar, 390 Cid: *rec.Cid, 391 ContentMode: genRec.ContentMode, 392 Creator: profile, 393 Description: genRec.Description, 394 DescriptionFacets: genRec.DescriptionFacets, 395 Did: did, 396 DisplayName: genRec.DisplayName, 397 IndexedAt: genRec.CreatedAt, 398 Uri: rec.Uri, 399 } 400 }(i, raw) 401 } 402 403 wg.Wait() 404 405 // build final slice 406 out := make([]*appbsky.FeedDefs_GeneratorView, 0, len(results)) 407 for _, r := range results { 408 if r.view != nil { 409 out = append(out, r.view) 410 } 411 } 412 413 c.JSON(http.StatusOK, &appbsky.FeedGetFeedGenerators_Output{ 414 Feeds: out, 415 }) 416 }) 417 418 router.GET("/xrpc/app.bsky.feed.getPosts", 419 func(c *gin.Context) { 420 rawdid := c.GetString("user_did") 421 var viewer *utils.DID 422 didval, errdid := utils.NewDID(rawdid) 423 if errdid != nil { 424 viewer = nil 425 } else { 426 viewer = &didval 427 } 428 postsreq := c.QueryArray("uris") 429 ctx := c.Request.Context() 430 431 type result struct { 432 view *appbsky.FeedDefs_PostView 433 } 434 435 results := make([]result, len(postsreq)) 436 437 var wg sync.WaitGroup 438 wg.Add(len(postsreq)) 439 440 for i, raw := range postsreq { 441 go func(i int, raw string) { 442 defer wg.Done() 443 444 post, _, _ := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 445 446 results[i].view = post 447 }(i, raw) 448 } 449 450 wg.Wait() 451 452 // build final slice 453 out := make([]*appbsky.FeedDefs_PostView, 0, len(results)) 454 for _, r := range results { 455 if r.view != nil { 456 out = append(out, r.view) 457 } 458 } 459 460 c.JSON(http.StatusOK, &appbsky.FeedGetPosts_Output{ 461 Posts: out, 462 }) 463 }) 464 465 router_unsafe.GET("/xrpc/app.bsky.feed.getFeed", 466 func(c *gin.Context) { 467 ctx := c.Request.Context() 468 469 rawdid := c.GetString("user_did") 470 log.Println("getFeed router_unsafe user_did: " + rawdid) 471 var viewer *utils.DID 472 didval, errdid := utils.NewDID(rawdid) 473 if errdid != nil { 474 viewer = nil 475 } else { 476 viewer = &didval 477 } 478 479 feedGenAturiRaw := c.Query("feed") 480 if feedGenAturiRaw == "" { 481 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 482 return 483 } 484 485 feedGenAturi, err := syntax.ParseATURI(feedGenAturiRaw) 486 if err != nil { 487 return 488 } 489 490 feedGeneratorRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.generator", feedGenAturi.Authority().String(), feedGenAturi.RecordKey().String()) 491 if err != nil { 492 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve feed generator record: %v", err)}) 493 return 494 } 495 496 var feedGeneratorRecord appbsky.FeedGenerator 497 if err := json.Unmarshal(*feedGeneratorRecordResponse.Value, &feedGeneratorRecord); err != nil { 498 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse feed generator record JSON"}) 499 return 500 } 501 502 feedGenDID := feedGeneratorRecord.Did 503 504 didDoc, err := ResolveDID(feedGenDID) 505 if err != nil { 506 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)}) 507 return 508 } 509 510 if err != nil { 511 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)}) 512 return 513 } 514 515 var targetEndpoint string 516 for _, svc := range didDoc.Service { 517 if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") { 518 targetEndpoint = svc.ServiceEndpoint 519 break 520 } 521 } 522 if targetEndpoint == "" { 523 c.JSON(http.StatusBadGateway, gin.H{"error": "Feed Generator service endpoint not found in DID document"}) 524 return 525 } 526 upstreamURL := fmt.Sprintf("%s/xrpc/app.bsky.feed.getFeedSkeleton?%s", 527 strings.TrimSuffix(targetEndpoint, "/"), 528 c.Request.URL.RawQuery, 529 ) 530 req, err := http.NewRequestWithContext(ctx, "GET", upstreamURL, nil) 531 if err != nil { 532 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upstream request"}) 533 return 534 } 535 headersToForward := []string{"Authorization", "Content-Type", "Accept", "User-Agent"} 536 for _, k := range headersToForward { 537 if v := c.GetHeader(k); v != "" { 538 req.Header.Set(k, v) 539 } 540 } 541 client := &http.Client{} 542 resp, err := client.Do(req) 543 if err != nil { 544 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Upstream request failed: %v", err)}) 545 return 546 } 547 defer resp.Body.Close() 548 549 bodyBytes, err := io.ReadAll(resp.Body) 550 if err != nil { 551 c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read upstream body"}) 552 return 553 } 554 if resp.StatusCode != http.StatusOK { 555 // Forward the upstream error raw 556 c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), bodyBytes) 557 return 558 } 559 560 var feekskeleton appbsky.FeedGetFeedSkeleton_Output 561 if err := json.Unmarshal(bodyBytes, &feekskeleton); err != nil { 562 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream JSON"}) 563 return 564 } 565 566 skeletonposts := feekskeleton.Feed 567 568 concurrentResults := MapConcurrent( 569 ctx, 570 skeletonposts, 571 20, 572 func(ctx context.Context, raw *appbsky.FeedDefs_SkeletonFeedPost) (*appbsky.FeedDefs_FeedViewPost, error) { 573 post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 574 if err != nil { 575 return nil, err 576 } 577 if post == nil { 578 return nil, fmt.Errorf("post not found") 579 } 580 581 return &appbsky.FeedDefs_FeedViewPost{ 582 // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"` 583 // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 584 Post: post, 585 // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"` 586 // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{ 587 // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost 588 // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{ 589 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"` 590 // LexiconTypeID: "app.bsky.feed.defs#reasonRepost", 591 // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"` 592 // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 593 // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 594 // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"` 595 // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost, 596 // }, 597 // // FeedDefs_ReasonPin *FeedDefs_ReasonPin 598 // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{ 599 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"` 600 // LexiconTypeID: "app.bsky.feed.defs#reasonPin", 601 // }, 602 // }, 603 // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"` 604 // // reqId: Unique identifier per request that may be passed back alongside interactions. 605 // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"` 606 }, nil 607 }, 608 ) 609 610 // build final slice 611 out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults)) 612 for _, r := range concurrentResults { 613 if r.Err == nil && r.Value != nil && r.Value.Post != nil { 614 out = append(out, r.Value) 615 } 616 } 617 618 c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{ 619 Cursor: feekskeleton.Cursor, 620 Feed: out, 621 }) 622 }) 623 type GetPostThreadOtherV2_Output_WithOtherReplies struct { 624 appbsky.UnspeccedGetPostThreadOtherV2_Output 625 HasOtherReplies bool `json:"hasOtherReplies"` 626 } 627 router.GET("/xrpc/app.bsky.unspecced.getPostThreadV2", 628 func(c *gin.Context) { 629 ctx := c.Request.Context() 630 631 rawdid := c.GetString("user_did") 632 var viewer *utils.DID 633 didval, errdid := utils.NewDID(rawdid) 634 if errdid != nil { 635 viewer = nil 636 } else { 637 viewer = &didval 638 } 639 640 threadAnchorURIraw := c.Query("anchor") 641 if threadAnchorURIraw == "" { 642 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 643 return 644 } 645 646 threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 647 if err != nil { 648 return 649 } 650 651 //var thread []*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem 652 653 var skeletonposts []string 654 skeletonposts = append(skeletonposts, threadAnchorURI.String()) 655 656 emptystrarray := &[]string{} 657 limit := 100 658 659 // todo: theres a cursor!!! pagination please! 660 // todo: also i doubt im gonna do proper threadding so make sure to remind me to do it properly thanks 661 //rootReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, nil) 662 parentReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, nil) 663 664 for _, rec := range parentReplies.Records { 665 recordaturi, err := syntax.ParseATURI("at://" + rec.Did + "/" + rec.Collection + "/" + rec.Rkey) 666 if err != nil { 667 continue 668 } 669 skeletonposts = append(skeletonposts, recordaturi.String()) 670 } 671 concurrentResults := MapConcurrent( 672 ctx, 673 skeletonposts, 674 20, 675 func(ctx context.Context, raw string) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) { 676 post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 3) 677 if err != nil { 678 return nil, err 679 } 680 if post == nil { 681 return nil, fmt.Errorf("post not found") 682 } 683 684 depth := int64(1) 685 if raw == threadAnchorURI.String() { 686 depth = 0 687 } 688 689 return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{ 690 // Depth int64 `json:"depth" cborgen:"depth"` 691 Depth: depth, // todo: placeholder 692 // Uri string `json:"uri" cborgen:"uri"` 693 Uri: raw, 694 // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"` 695 Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{ 696 // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost 697 UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{ 698 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"` 699 LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost", 700 // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. 701 // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"` 702 HiddenByThreadgate: false, // todo: placeholder 703 // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents. 704 // MoreParents bool `json:"moreParents" cborgen:"moreParents"` 705 MoreParents: false, // todo: placeholder 706 // // moreReplies: This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate. 707 // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"` 708 MoreReplies: 0, // todo: placeholder 709 // // mutedByViewer: This is by an account muted by the viewer requesting it. 710 // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"` 711 MutedByViewer: false, // todo: placeholder 712 // // opThread: This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread. 713 // OpThread bool `json:"opThread" cborgen:"opThread"` 714 OpThread: false, // todo: placeholder 715 // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 716 Post: post, 717 }, 718 }, 719 }, nil 720 }, 721 ) 722 723 // build final slice 724 out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults)) 725 for _, r := range concurrentResults { 726 if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil { 727 out = append(out, r.Value) 728 } 729 } 730 731 // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{ 732 // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"` 733 // Thread: out, 734 // HasOtherReplies: false, 735 // }) 736 resp := &GetPostThreadOtherV2_Output_WithOtherReplies{ 737 UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{ 738 Thread: out, 739 }, 740 HasOtherReplies: false, 741 } 742 c.JSON(http.StatusOK, resp) 743 }) 744 745 // weird stuff 746 yourJSONBytes, _ := os.ReadFile("./public/getConfig.json") 747 router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) { 748 c.DataFromReader(200, -1, "application/json", 749 bytes.NewReader(yourJSONBytes), nil) 750 }) 751 752 router.GET("/", func(c *gin.Context) { 753 log.Println("hello worldio !") 754 clientUUID := sticket.GetUUIDFromRequest(c.Request) 755 hasSticket := clientUUID != "" 756 if hasSticket { 757 go func(targetUUID string) { 758 // simulated heavy processing 759 time.Sleep(2 * time.Second) 760 761 lateData := map[string]any{ 762 "postId": 101, 763 "newComments": []string{ 764 "Wow great tutorial!", 765 "I am stuck on step 1.", 766 }, 767 } 768 769 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 770 if success { 771 log.Println("Successfully sent late data via Sticket") 772 } else { 773 log.Println("Failed to send late data (client disconnected?)") 774 } 775 }(clientUUID) 776 } 777 }) 778 router_raw.Run(":7152") 779} 780 781func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 782 log.Println("hello worldio !") 783} 784 785type DidResponse struct { 786 Context []string `json:"@context"` 787 ID string `json:"id"` 788 Service []did.Service `json:"service"` 789} 790 791/* 792 { 793 id: "#bsky_appview", 794 type: "BskyAppView", 795 serviceEndpoint: endpoint, 796 }, 797*/ 798func GetWellKnownDID(c *gin.Context) { 799 // Use a custom struct to fix missing omitempty on did.Document 800 serviceEndpoint := serviceWebHost 801 serviceDID, err := did.ParseDID(serviceWebDID) 802 if err != nil { 803 log.Println(fmt.Errorf("error parsing serviceDID: %w", err)) 804 return 805 } 806 serviceID, err := did.ParseDID("#bsky_appview") 807 if err != nil { 808 panic(err) 809 } 810 didDoc := did.Document{ 811 Context: []string{did.CtxDIDv1}, 812 ID: serviceDID, 813 Service: []did.Service{ 814 { 815 ID: serviceID, 816 Type: "BskyAppView", 817 ServiceEndpoint: serviceEndpoint, 818 }, 819 }, 820 } 821 didResponse := DidResponse{ 822 Context: didDoc.Context, 823 ID: didDoc.ID.String(), 824 Service: didDoc.Service, 825 } 826 c.JSON(http.StatusOK, didResponse) 827}