bluesky appview implementation using microcosm and other services server.reddwarf.app
appview bluesky reddwarf microcosm
at main 29 kB view raw
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 aturilist "tangled.org/whey.party/red-dwarf-server/cmd/aturilist/client" 20 "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 21 "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 22 appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs" 23 appbskyfeeddefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/feed/defs" 24 appbskyunspeccedgetpostthreadv2 "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/unspecced/getpostthreadv2" 25 "tangled.org/whey.party/red-dwarf-server/shims/utils" 26 "tangled.org/whey.party/red-dwarf-server/sticket" 27 "tangled.org/whey.party/red-dwarf-server/store" 28 29 // "github.com/bluesky-social/indigo/atproto/atclient" 30 // comatproto "github.com/bluesky-social/indigo/api/atproto" 31 appbsky "github.com/bluesky-social/indigo/api/bsky" 32 "github.com/bluesky-social/indigo/atproto/syntax" 33 34 // "github.com/bluesky-social/indigo/atproto/atclient" 35 // "github.com/bluesky-social/indigo/atproto/identity" 36 // "github.com/bluesky-social/indigo/atproto/syntax" 37 "github.com/bluesky-social/indigo/api/agnostic" 38 "github.com/gin-contrib/cors" 39 "github.com/gin-gonic/gin" 40 // "github.com/bluesky-social/jetstream/pkg/models" 41) 42 43var ( 44 JETSTREAM_URL string 45 SPACEDUST_URL string 46 SLINGSHOT_URL string 47 CONSTELLATION_URL string 48 ATURILIST_URL string 49) 50 51func initURLs(prod bool) { 52 if !prod { 53 JETSTREAM_URL = "wss://jetstream.whey.party/subscribe" 54 SPACEDUST_URL = "wss://spacedust.whey.party/subscribe" 55 SLINGSHOT_URL = "https://slingshot.whey.party" 56 CONSTELLATION_URL = "https://constellation.whey.party" 57 ATURILIST_URL = "http://localhost:7155" 58 } else { 59 JETSTREAM_URL = "ws://localhost:6008/subscribe" 60 SPACEDUST_URL = "ws://localhost:9998/subscribe" 61 SLINGSHOT_URL = "http://localhost:7729" 62 CONSTELLATION_URL = "http://localhost:7728" 63 ATURILIST_URL = "http://localhost:7155" 64 } 65} 66 67const ( 68 BSKYIMAGECDN_URL = "https://cdn.bsky.app" 69 BSKYVIDEOCDN_URL = "https://video.bsky.app" 70 serviceWebDID = "did:web:server.reddwarf.app" 71 serviceWebHost = "https://server.reddwarf.app" 72) 73 74func main() { 75 log.Println("red-dwarf-server AppView Service started") 76 prod := flag.Bool("prod", false, "use production URLs instead of localhost") 77 flag.Parse() 78 79 initURLs(*prod) 80 81 ctx := context.Background() 82 mailbox := sticket.New() 83 sl := slingshot.NewSlingshot(SLINGSHOT_URL) 84 cs := constellation.NewConstellation(CONSTELLATION_URL) 85 al := aturilist.NewClient(ATURILIST_URL) 86 // spacedust is type definitions only 87 // jetstream types is probably available from jetstream/pkg/models 88 89 //threadGraphCache := make(map[syntax.ATURI]appbskyunspeccedgetpostthreadv2.ThreadGraph) 90 91 router_raw := gin.New() 92 router_raw.Use(gin.Logger()) 93 router_raw.Use(gin.Recovery()) 94 //router_raw.Use(cors.Default()) 95 router_raw.Use(cors.New(cors.Config{ 96 AllowAllOrigins: true, 97 AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, 98 // You must explicitly allow the custom ATProto headers here 99 AllowHeaders: []string{ 100 "Origin", 101 "Content-Length", 102 "Content-Type", 103 "Authorization", 104 "Accept", 105 "Accept-Language", 106 "atproto-accept-labelers", // <--- The specific fix for your error 107 "atproto-proxy", // Good to have for future compatibility 108 }, 109 ExposeHeaders: []string{"Content-Length", "Link"}, 110 AllowCredentials: true, 111 MaxAge: 12 * time.Hour, 112 })) 113 114 router_raw.GET("/.well-known/did.json", GetWellKnownDID) 115 116 auther, err := auth.NewAuth( 117 100_000, 118 time.Hour*12, 119 5, 120 serviceWebDID, //+"#bsky_appview", 121 ) 122 if err != nil { 123 log.Fatalf("Failed to create Auth: %v", err) 124 } 125 126 router := router_raw.Group("/") 127 router.Use(auther.AuthenticateGinRequestViaJWT) 128 129 router_unsafe := router_raw.Group("/") 130 router_unsafe.Use(auther.AuthenticateGinRequestViaJWTUnsafe) 131 132 responsewow, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", "did:web:did12.whey.party", "self") 133 if err != nil { 134 log.Println(err) 135 } 136 137 log.Println(responsewow.Uri) 138 139 var didtest *utils.DID 140 didval, errdid := utils.NewDID("did:web:did12.whey.party") 141 if errdid != nil { 142 didtest = nil 143 } else { 144 didtest = &didval 145 } 146 profiletest, _, _ := appbskyactordefs.ProfileViewBasic(ctx, *didtest, sl, cs, BSKYIMAGECDN_URL, nil) 147 148 log.Println(*profiletest.DisplayName) 149 log.Println(*profiletest.Avatar) 150 151 router.GET("/ws", func(c *gin.Context) { 152 mailbox.HandleWS(c.Writer, c.Request) 153 }) 154 155 kv := store.NewKV() 156 157 // sad attempt to get putpref working. tldr it wont work without a client fork 158 // https://bsky.app/profile/did:web:did12.whey.party/post/3m75xtomd722n 159 router.GET("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 160 c.Status(200) 161 }) 162 router.PUT("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 163 c.Status(200) 164 }) 165 router.POST("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 166 c.Status(200) 167 168 userDID := c.GetString("user_did") 169 body, err := io.ReadAll(c.Request.Body) 170 if err != nil { 171 c.JSON(400, gin.H{"error": "invalid body"}) 172 return 173 } 174 175 kv.Set(userDID, body) 176 177 }) 178 179 router.GET("/xrpc/app.bsky.actor.getPreferences", func(c *gin.Context) { 180 userDID := c.GetString("user_did") 181 val, ok := kv.Get(userDID) 182 if !ok { 183 c.JSON(200, gin.H{"preferences": []any{}}) 184 return 185 } 186 187 c.Data(200, "application/json", val) 188 189 }) 190 191 bskyappdid, _ := utils.NewDID("did:plc:z72i7hdynmk6r22z27h6tvur") 192 193 profiletest2, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, bskyappdid, sl, cs, BSKYIMAGECDN_URL, nil) 194 195 data, err := json.MarshalIndent(profiletest2, "", " ") 196 if err != nil { 197 panic(err) 198 } 199 fmt.Println(string(data)) 200 201 router.GET("/xrpc/app.bsky.actor.getProfiles", 202 func(c *gin.Context) { 203 actors := c.QueryArray("actors") 204 205 rawdid := c.GetString("user_did") 206 var viewer *utils.DID 207 didval, errdid := utils.NewDID(rawdid) 208 if errdid != nil { 209 viewer = nil 210 } else { 211 viewer = &didval 212 } 213 214 profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors)) 215 216 for _, v := range actors { 217 did, err := utils.NewDID(v) 218 if err != nil { 219 continue 220 } 221 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 222 profiles = append(profiles, profile) 223 } 224 225 c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{ 226 Profiles: profiles, 227 }) 228 }) 229 230 router.GET("/xrpc/app.bsky.actor.getProfile", 231 func(c *gin.Context) { 232 actor := c.Query("actor") 233 did, err := utils.NewDID(actor) 234 if err != nil { 235 c.JSON(http.StatusBadRequest, nil) 236 return 237 } 238 rawdid := c.GetString("user_did") 239 var viewer *utils.DID 240 didval, errdid := utils.NewDID(rawdid) 241 if errdid != nil { 242 viewer = nil 243 } else { 244 viewer = &didval 245 } 246 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 247 c.JSON(http.StatusOK, profile) 248 }) 249 250 // really bad actually 251 router.GET("/xrpc/app.bsky.notification.listNotifications", 252 func(c *gin.Context) { 253 emptyarray := []*appbsky.NotificationListNotifications_Notification{} 254 notifshim := &appbsky.NotificationListNotifications_Output{ 255 Notifications: emptyarray, 256 } 257 c.JSON(http.StatusOK, notifshim) 258 }) 259 260 router.GET("/xrpc/app.bsky.labeler.getServices", 261 func(c *gin.Context) { 262 dids := c.QueryArray("dids") 263 264 labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids)) 265 //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids)) 266 267 for _, v := range dids { 268 did, err := utils.NewDID(v) 269 if err != nil { 270 continue 271 } 272 rawdid := c.GetString("user_did") 273 var viewer *utils.DID 274 didval, errdid := utils.NewDID(rawdid) 275 if errdid != nil { 276 viewer = nil 277 } else { 278 viewer = &didval 279 } 280 labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, cs, BSKYIMAGECDN_URL, viewer) 281 labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self") 282 var labelerservice appbsky.LabelerService 283 if labelerserviceresponse != nil { 284 if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil { 285 continue 286 } 287 } 288 289 a := "account" 290 b := "record" 291 c := "chat" 292 293 placeholderTypes := []*string{&a, &b, &c} 294 295 labeler := &appbsky.LabelerGetServices_Output_Views_Elem{ 296 LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{ 297 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"` 298 LexiconTypeID: "app.bsky.labeler.defs#labelerView", 299 // Cid string `json:"cid" cborgen:"cid"` 300 Cid: *labelerserviceresponse.Cid, 301 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 302 Creator: labelerprofile, 303 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 304 IndexedAt: labelerservice.CreatedAt, 305 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 306 Labels: nil, // seems to always be empty? 307 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 308 LikeCount: nil, // placeholder sorry 309 // Uri string `json:"uri" cborgen:"uri"` 310 Uri: labelerserviceresponse.Uri, 311 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 312 Viewer: nil, 313 }, 314 LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{ 315 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"` 316 LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed", 317 // Cid string `json:"cid" cborgen:"cid"` 318 Cid: *labelerserviceresponse.Cid, 319 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 320 Creator: labelerprofile, 321 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 322 IndexedAt: labelerservice.CreatedAt, 323 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 324 Labels: nil, // seems to always be empty? 325 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 326 LikeCount: nil, // placeholder sorry 327 // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"` 328 Policies: labelerservice.Policies, 329 // // 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. 330 // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"` 331 ReasonTypes: nil, //usually not even present 332 // // 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. 333 // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"` 334 SubjectCollections: nil, //usually not even present 335 // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on. 336 // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"` 337 SubjectTypes: placeholderTypes, 338 // Uri string `json:"uri" cborgen:"uri"` 339 Uri: labelerserviceresponse.Uri, 340 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 341 Viewer: nil, 342 }, 343 } 344 labelers = append(labelers, labeler) 345 } 346 347 c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{ 348 Views: labelers, 349 }) 350 }) 351 352 router.GET("/xrpc/app.bsky.feed.getFeedGenerators", 353 func(c *gin.Context) { 354 feeds := c.QueryArray("feeds") 355 ctx := c.Request.Context() 356 357 type result struct { 358 view *appbsky.FeedDefs_GeneratorView 359 } 360 361 results := make([]result, len(feeds)) 362 363 var wg sync.WaitGroup 364 wg.Add(len(feeds)) 365 366 for i, raw := range feeds { 367 go func(i int, raw string) { 368 defer wg.Done() 369 370 aturi, err := syntax.ParseATURI(raw) 371 if err != nil { 372 return 373 } 374 375 did := aturi.Authority().String() 376 collection := aturi.Collection().String() 377 rkey := aturi.RecordKey().String() 378 379 repoDID, err := utils.NewDID(did) 380 if err != nil { 381 return 382 } 383 rawdid := c.GetString("user_did") 384 var viewer *utils.DID 385 didval, errdid := utils.NewDID(rawdid) 386 if errdid != nil { 387 viewer = nil 388 } else { 389 viewer = &didval 390 } 391 392 // fetch profile and record in parallel too (optional) 393 // but to keep it simple, do serial inside this goroutine 394 profile, _, _ := appbskyactordefs.ProfileView(ctx, repoDID, sl, cs, BSKYIMAGECDN_URL, viewer) 395 396 rec, err := agnostic.RepoGetRecord(ctx, sl, "", collection, did, rkey) 397 if err != nil || rec.Value == nil { 398 return 399 } 400 401 var genRec appbsky.FeedGenerator 402 if err := json.Unmarshal(*rec.Value, &genRec); err != nil { 403 return 404 } 405 406 var avatar *string 407 if genRec.Avatar != nil { 408 u := utils.MakeImageCDN(repoDID, BSKYIMAGECDN_URL, "avatar", genRec.Avatar.Ref.String()) 409 avatar = &u 410 } 411 412 results[i].view = &appbsky.FeedDefs_GeneratorView{ 413 LexiconTypeID: "app.bsky.feed.defs#generatorView", 414 AcceptsInteractions: genRec.AcceptsInteractions, 415 Avatar: avatar, 416 Cid: *rec.Cid, 417 ContentMode: genRec.ContentMode, 418 Creator: profile, 419 Description: genRec.Description, 420 DescriptionFacets: genRec.DescriptionFacets, 421 Did: did, 422 DisplayName: genRec.DisplayName, 423 IndexedAt: genRec.CreatedAt, 424 Uri: rec.Uri, 425 } 426 }(i, raw) 427 } 428 429 wg.Wait() 430 431 // build final slice 432 out := make([]*appbsky.FeedDefs_GeneratorView, 0, len(results)) 433 for _, r := range results { 434 if r.view != nil { 435 out = append(out, r.view) 436 } 437 } 438 439 c.JSON(http.StatusOK, &appbsky.FeedGetFeedGenerators_Output{ 440 Feeds: out, 441 }) 442 }) 443 444 router.GET("/xrpc/app.bsky.feed.getPosts", 445 func(c *gin.Context) { 446 rawdid := c.GetString("user_did") 447 var viewer *utils.DID 448 didval, errdid := utils.NewDID(rawdid) 449 if errdid != nil { 450 viewer = nil 451 } else { 452 viewer = &didval 453 } 454 postsreq := c.QueryArray("uris") 455 ctx := c.Request.Context() 456 457 type result struct { 458 view *appbsky.FeedDefs_PostView 459 } 460 461 results := make([]result, len(postsreq)) 462 463 var wg sync.WaitGroup 464 wg.Add(len(postsreq)) 465 466 for i, raw := range postsreq { 467 go func(i int, raw string) { 468 defer wg.Done() 469 470 post, _, _ := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 471 472 results[i].view = post 473 }(i, raw) 474 } 475 476 wg.Wait() 477 478 // build final slice 479 out := make([]*appbsky.FeedDefs_PostView, 0, len(results)) 480 for _, r := range results { 481 if r.view != nil { 482 out = append(out, r.view) 483 } 484 } 485 486 c.JSON(http.StatusOK, &appbsky.FeedGetPosts_Output{ 487 Posts: out, 488 }) 489 }) 490 491 router_unsafe.GET("/xrpc/app.bsky.feed.getFeed", 492 func(c *gin.Context) { 493 ctx := c.Request.Context() 494 495 rawdid := c.GetString("user_did") 496 log.Println("getFeed router_unsafe user_did: " + rawdid) 497 var viewer *utils.DID 498 didval, errdid := utils.NewDID(rawdid) 499 if errdid != nil { 500 viewer = nil 501 } else { 502 viewer = &didval 503 } 504 505 feedGenAturiRaw := c.Query("feed") 506 if feedGenAturiRaw == "" { 507 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 508 return 509 } 510 511 feedGenAturi, err := syntax.ParseATURI(feedGenAturiRaw) 512 if err != nil { 513 return 514 } 515 516 feedGeneratorRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.generator", feedGenAturi.Authority().String(), feedGenAturi.RecordKey().String()) 517 if err != nil { 518 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve feed generator record: %v", err)}) 519 return 520 } 521 522 var feedGeneratorRecord appbsky.FeedGenerator 523 if err := json.Unmarshal(*feedGeneratorRecordResponse.Value, &feedGeneratorRecord); err != nil { 524 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse feed generator record JSON"}) 525 return 526 } 527 528 feedGenDID := feedGeneratorRecord.Did 529 530 didDoc, err := ResolveDID(feedGenDID) 531 if err != nil { 532 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to resolve DID: %v", err)}) 533 return 534 } 535 536 var targetEndpoint string 537 for _, svc := range didDoc.Service { 538 if svc.Type == "BskyFeedGenerator" && strings.HasSuffix(svc.ID, "#bsky_fg") { 539 targetEndpoint = svc.ServiceEndpoint 540 break 541 } 542 } 543 if targetEndpoint == "" { 544 c.JSON(http.StatusBadGateway, gin.H{"error": "Feed Generator service endpoint not found in DID document"}) 545 return 546 } 547 upstreamURL := fmt.Sprintf("%s/xrpc/app.bsky.feed.getFeedSkeleton?%s", 548 strings.TrimSuffix(targetEndpoint, "/"), 549 c.Request.URL.RawQuery, 550 ) 551 req, err := http.NewRequestWithContext(ctx, "GET", upstreamURL, nil) 552 if err != nil { 553 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upstream request"}) 554 return 555 } 556 headersToForward := []string{"Authorization", "Content-Type", "Accept", "User-Agent"} 557 for _, k := range headersToForward { 558 if v := c.GetHeader(k); v != "" { 559 req.Header.Set(k, v) 560 } 561 } 562 client := &http.Client{} 563 resp, err := client.Do(req) 564 if err != nil { 565 c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Upstream request failed: %v", err)}) 566 return 567 } 568 defer resp.Body.Close() 569 570 bodyBytes, err := io.ReadAll(resp.Body) 571 if err != nil { 572 c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read upstream body"}) 573 return 574 } 575 if resp.StatusCode != http.StatusOK { 576 // Forward the upstream error raw 577 c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), bodyBytes) 578 return 579 } 580 581 var feekskeleton appbsky.FeedGetFeedSkeleton_Output 582 if err := json.Unmarshal(bodyBytes, &feekskeleton); err != nil { 583 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse upstream JSON"}) 584 return 585 } 586 587 skeletonposts := feekskeleton.Feed 588 589 concurrentResults := MapConcurrent( 590 ctx, 591 skeletonposts, 592 20, 593 func(ctx context.Context, raw *appbsky.FeedDefs_SkeletonFeedPost, idx int) (*appbsky.FeedDefs_FeedViewPost, error) { 594 post, _, err := appbskyfeeddefs.PostView(ctx, raw.Post, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 595 if err != nil { 596 return nil, err 597 } 598 if post == nil { 599 return nil, fmt.Errorf("post not found") 600 } 601 602 return &appbsky.FeedDefs_FeedViewPost{ 603 // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"` 604 // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 605 Post: post, 606 // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"` 607 // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{ 608 // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost 609 // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{ 610 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"` 611 // LexiconTypeID: "app.bsky.feed.defs#reasonRepost", 612 // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"` 613 // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 614 // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 615 // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"` 616 // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost, 617 // }, 618 // // FeedDefs_ReasonPin *FeedDefs_ReasonPin 619 // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{ 620 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"` 621 // LexiconTypeID: "app.bsky.feed.defs#reasonPin", 622 // }, 623 // }, 624 // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"` 625 // // reqId: Unique identifier per request that may be passed back alongside interactions. 626 // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"` 627 }, nil 628 }, 629 ) 630 631 // build final slice 632 out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults)) 633 for _, r := range concurrentResults { 634 if r.Err == nil && r.Value != nil && r.Value.Post != nil { 635 out = append(out, r.Value) 636 } 637 } 638 639 c.JSON(http.StatusOK, &appbsky.FeedGetFeed_Output{ 640 Cursor: feekskeleton.Cursor, 641 Feed: out, 642 }) 643 }) 644 type GetPostThreadOtherV2_Output_WithOtherReplies struct { 645 appbsky.UnspeccedGetPostThreadOtherV2_Output 646 HasOtherReplies bool `json:"hasOtherReplies"` 647 } 648 router.GET("/xrpc/app.bsky.unspecced.getPostThreadV2", 649 func(c *gin.Context) { 650 //appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2(c, sl, cs, BSKYIMAGECDN_URL) 651 // V2V2 still doesnt work. should probably make the handler from scratch to fully use the thread grapher. 652 // also the thread grapher is still sequental. pls fix that 653 //appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2V2(c, sl, cs, BSKYIMAGECDN_URL) 654 655 var existingGraph *appbskyunspeccedgetpostthreadv2.ThreadGraph 656 // var kvkey string 657 // threadAnchorURIraw := c.Query("anchor") 658 // if threadAnchorURIraw != "" { 659 // threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 660 // if err == nil { 661 // kvkey = "ThreadGraph" + threadAnchorURI.String() 662 // val, ok := kv.Get(kvkey) 663 // if ok { 664 // parsed, err := appbskyunspeccedgetpostthreadv2.ThreadGraphFromBytes(val) 665 // if err != nil { 666 // existingGraph = parsed 667 // } 668 // } 669 // } 670 // } 671 672 returnedGraph := appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2V3(c, sl, cs, BSKYIMAGECDN_URL, existingGraph) 673 _ = returnedGraph 674 // bytes, err := returnedGraph.ToBytes() 675 // if err == nil && kvkey != "" { 676 // kv.Set(kvkey, bytes, 1*time.Minute) 677 // } 678 }) 679 680 router.GET("/xrpc/app.bsky.feed.getAuthorFeed", 681 func(c *gin.Context) { 682 683 rawdid := c.GetString("user_did") 684 log.Println("getFeed router_unsafe user_did: " + rawdid) 685 var viewer *utils.DID 686 didval, errdid := utils.NewDID(rawdid) 687 if errdid != nil { 688 viewer = nil 689 } else { 690 viewer = &didval 691 } 692 693 actorDidParam := c.Query("actor") 694 if actorDidParam == "" { 695 c.JSON(http.StatusBadRequest, gin.H{"error": "Missing actor param"}) 696 return 697 } 698 cursorRawParam := c.Query("cursor") 699 700 listResp, err := al.ListRecords(ctx, actorDidParam, "app.bsky.feed.post", cursorRawParam, true) 701 if err != nil { 702 log.Fatalf("Failed to list: %v", err) 703 } 704 705 concurrentResults := MapConcurrent( 706 ctx, 707 listResp.Aturis, 708 20, 709 func(ctx context.Context, raw string, idx int) (*appbsky.FeedDefs_FeedViewPost, error) { 710 post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 2) 711 if err != nil { 712 return nil, err 713 } 714 if post == nil { 715 return nil, fmt.Errorf("post not found") 716 } 717 718 return &appbsky.FeedDefs_FeedViewPost{ 719 // FeedContext *string `json:"feedContext,omitempty" cborgen:"feedContext,omitempty"` 720 // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 721 Post: post, 722 // Reason *FeedDefs_FeedViewPost_Reason `json:"reason,omitempty" cborgen:"reason,omitempty"` 723 // Reason: &appbsky.FeedDefs_FeedViewPost_Reason{ 724 // // FeedDefs_ReasonRepost *FeedDefs_ReasonRepost 725 // FeedDefs_ReasonRepost: &appbsky.FeedDefs_ReasonRepost{ 726 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonRepost"` 727 // LexiconTypeID: "app.bsky.feed.defs#reasonRepost", 728 // // By *ActorDefs_ProfileViewBasic `json:"by" cborgen:"by"` 729 // // Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 730 // // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 731 // // Uri *string `json:"uri,omitempty" cborgen:"uri,omitempty"` 732 // Uri: &raw.Reason.FeedDefs_SkeletonReasonRepost.Repost, 733 // }, 734 // // FeedDefs_ReasonPin *FeedDefs_ReasonPin 735 // FeedDefs_ReasonPin: &appbsky.FeedDefs_ReasonPin{ 736 // // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.feed.defs#reasonPin"` 737 // LexiconTypeID: "app.bsky.feed.defs#reasonPin", 738 // }, 739 // }, 740 // Reply *FeedDefs_ReplyRef `json:"reply,omitempty" cborgen:"reply,omitempty"` 741 // // reqId: Unique identifier per request that may be passed back alongside interactions. 742 // ReqId *string `json:"reqId,omitempty" cborgen:"reqId,omitempty"` 743 }, nil 744 }, 745 ) 746 747 // build final slice 748 out := make([]*appbsky.FeedDefs_FeedViewPost, 0, len(concurrentResults)) 749 for _, r := range concurrentResults { 750 if r.Err == nil && r.Value != nil && r.Value.Post != nil { 751 out = append(out, r.Value) 752 } 753 } 754 755 c.JSON(http.StatusOK, &appbsky.FeedGetAuthorFeed_Output{ 756 Cursor: &listResp.Cursor, 757 Feed: out, 758 }) 759 }) 760 761 // weird stuff 762 yourJSONBytes, _ := os.ReadFile("./public/getConfig.json") 763 router.GET("/xrpc/app.bsky.unspecced.getConfig", func(c *gin.Context) { 764 c.DataFromReader(200, -1, "application/json", 765 bytes.NewReader(yourJSONBytes), nil) 766 }) 767 768 router.GET("/", func(c *gin.Context) { 769 log.Println("hello worldio !") 770 clientUUID := sticket.GetUUIDFromRequest(c.Request) 771 hasSticket := clientUUID != "" 772 if hasSticket { 773 go func(targetUUID string) { 774 // simulated heavy processing 775 time.Sleep(2 * time.Second) 776 777 lateData := map[string]any{ 778 "postId": 101, 779 "newComments": []string{ 780 "Wow great tutorial!", 781 "I am stuck on step 1.", 782 }, 783 } 784 785 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 786 if success { 787 log.Println("Successfully sent late data via Sticket") 788 } else { 789 log.Println("Failed to send late data (client disconnected?)") 790 } 791 }(clientUUID) 792 } 793 c.String(http.StatusOK, ` ____ __________ ____ _ _____ ____ ______ 794 / __ \/ ____/ __ \ / __ \ | / / | / __ \/ ____/ 795 / /_/ / __/ / / / / / / / / | /| / / /| | / /_/ / /_ 796 / _, _/ /___/ /_/ / / /_/ /| |/ |/ / ___ |/ _, _/ __/ 797/_/ |_/_____/_____/ /_____/ |__/|__/_/ |_/_/ |_/_/ 798 _____ __________ _ ____________ 799 / ___// ____/ __ \ | / / ____/ __ \ 800 \__ \/ __/ / /_/ / | / / __/ / /_/ / 801 ___/ / /___/ _, _/| |/ / /___/ _, _/ 802/____/_____/_/ |_| |___/_____/_/ |_| 803 804This is an AT Protocol Application View (AppView) for any application that supports app.bsky.* xrpc methods. 805 806Most API routes are under /xrpc/ 807 808 Code: https://tangled.org/whey.party/red-dwarf-server 809 Protocol: https://atproto.com 810 Try it on: https://new.reddwarf.app 811`) 812 }) 813 router_raw.Run(":7152") 814} 815 816func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 817 log.Println("hello worldio !") 818} 819 820type DidResponse struct { 821 Context []string `json:"@context"` 822 ID string `json:"id"` 823 Service []did.Service `json:"service"` 824} 825 826/* 827 { 828 id: "#bsky_appview", 829 type: "BskyAppView", 830 serviceEndpoint: endpoint, 831 }, 832*/ 833func GetWellKnownDID(c *gin.Context) { 834 // Use a custom struct to fix missing omitempty on did.Document 835 serviceEndpoint := serviceWebHost 836 serviceDID, err := did.ParseDID(serviceWebDID) 837 if err != nil { 838 log.Println(fmt.Errorf("error parsing serviceDID: %w", err)) 839 return 840 } 841 serviceID, err := did.ParseDID("#bsky_appview") 842 if err != nil { 843 panic(err) 844 } 845 didDoc := did.Document{ 846 Context: []string{did.CtxDIDv1}, 847 ID: serviceDID, 848 Service: []did.Service{ 849 { 850 ID: serviceID, 851 Type: "BskyAppView", 852 ServiceEndpoint: serviceEndpoint, 853 }, 854 }, 855 } 856 didResponse := DidResponse{ 857 Context: didDoc.Context, 858 ID: didDoc.ID.String(), 859 Service: didDoc.Service, 860 } 861 c.JSON(http.StatusOK, didResponse) 862}