collection of golang services under the Red Dwarf umbrella server.reddwarf.app
bluesky reddwarf microcosm appview
15
fork

Configure Feed

Select the types of activity you want to include in your feed.

getPostThreadV2 mostly full impl w/ op prio (kinda slow and no sort)

+680 -171
+8 -168
main.go
··· 20 20 "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 21 21 appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs" 22 22 appbskyfeeddefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/feed/defs" 23 + appbskyunspeccedgetpostthreadv2 "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/unspecced/getpostthreadv2" 23 24 "tangled.org/whey.party/red-dwarf-server/shims/utils" 24 25 "tangled.org/whey.party/red-dwarf-server/sticket" 25 26 "tangled.org/whey.party/red-dwarf-server/store" ··· 79 80 cs := constellation.NewConstellation(CONSTELLATION_URL) 80 81 // spacedust is type definitions only 81 82 // jetstream types is probably available from jetstream/pkg/models 83 + 84 + //threadGraphCache := make(map[syntax.ATURI]appbskyunspeccedgetpostthreadv2.ThreadGraph) 82 85 83 86 router_raw := gin.New() 84 87 router_raw.Use(gin.Logger()) ··· 626 629 } 627 630 router.GET("/xrpc/app.bsky.unspecced.getPostThreadV2", 628 631 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 - 655 - emptystrarray := &[]string{} 656 - limit := 100 657 - 658 - // recurse to the top 659 - parentsMap := map[string]*appbsky.FeedDefs_PostView{} 660 - current := threadAnchorURI 661 - iteration := 0 662 - for true { 663 - iteration = iteration + 1 664 - if iteration > 10 { 665 - break 666 - } 667 - recordaturi, err := syntax.ParseATURI("at://" + current.Authority().String() + "/" + string(current.Collection()) + "/" + string(current.RecordKey())) 668 - if err != nil { 669 - break 670 - } 671 - // loop 672 - postView, postRecord, err := appbskyfeeddefs.PostView(ctx, recordaturi.String(), sl, cs, BSKYIMAGECDN_URL, viewer, 3) 673 - if err != nil { 674 - break 675 - } 676 - if postView == nil { 677 - break 678 - } 679 - //if iteration != 1 { 680 - skeletonposts = append([]string{recordaturi.String()}, skeletonposts...) 681 - //} 682 - parentsMap[recordaturi.String()] = postView 683 - if postRecord != nil && postRecord.Reply != nil && postRecord.Reply.Parent != nil && postRecord.Reply.Root != nil { 684 - if postRecord.Reply.Parent.Uri == postRecord.Reply.Root.Uri { 685 - //break 686 - iteration = 9 687 - } 688 - 689 - current, err = syntax.ParseATURI(postRecord.Reply.Parent.Uri) 690 - if err != nil { 691 - break 692 - } 693 - } else { 694 - log.Println("what huh what hwat") 695 - break //whatt 696 - } 697 - 698 - } 699 - 700 - //skeletonposts = append(skeletonposts, threadAnchorURI.String()) 701 - // todo: theres a cursor!!! pagination please! 702 - // todo: also i doubt im gonna do proper threadding right now, so make sure to remind me to do it properly some time later thanks 703 - //rootReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, nil) 704 - parentReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, nil) 705 - 706 - for _, rec := range parentReplies.Records { 707 - recordaturi, err := syntax.ParseATURI("at://" + rec.Did + "/" + rec.Collection + "/" + rec.Rkey) 708 - if err != nil { 709 - continue 710 - } 711 - skeletonposts = append(skeletonposts, recordaturi.String()) 712 - } 713 - maplen := len(parentsMap) 714 - concurrentResults := MapConcurrent( 715 - ctx, 716 - skeletonposts, 717 - 20, 718 - func(ctx context.Context, raw string, idx int) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) { 719 - var postView *appbsky.FeedDefs_PostView 720 - fromParentChain := false 721 - postView, ok := parentsMap[raw] 722 - if !ok { 723 - post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, BSKYIMAGECDN_URL, viewer, 3) 724 - if err != nil { 725 - return nil, err 726 - } 727 - if post == nil { 728 - return nil, fmt.Errorf("post not found") 729 - } 730 - postView = post 731 - } else { 732 - fromParentChain = true 733 - } 734 - 735 - depth := int64(1) 736 - if raw == threadAnchorURI.String() { 737 - depth = 0 738 - } 739 - if fromParentChain { 740 - depth = int64(0 - maplen + idx + 1) 741 - } 742 - 743 - return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{ 744 - // Depth int64 `json:"depth" cborgen:"depth"` 745 - Depth: depth, // todo: placeholder 746 - // Uri string `json:"uri" cborgen:"uri"` 747 - Uri: raw, 748 - // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"` 749 - Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{ 750 - // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost 751 - UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{ 752 - // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"` 753 - LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost", 754 - // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. 755 - // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"` 756 - HiddenByThreadgate: false, // todo: placeholder 757 - // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents. 758 - // MoreParents bool `json:"moreParents" cborgen:"moreParents"` 759 - MoreParents: false, // todo: placeholder 760 - // // 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. 761 - // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"` 762 - MoreReplies: 0, // todo: placeholder 763 - // // mutedByViewer: This is by an account muted by the viewer requesting it. 764 - // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"` 765 - MutedByViewer: false, // todo: placeholder 766 - // // 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. 767 - // OpThread bool `json:"opThread" cborgen:"opThread"` 768 - OpThread: false, // todo: placeholder 769 - // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 770 - Post: postView, 771 - }, 772 - }, 773 - }, nil 774 - }, 775 - ) 776 - 777 - // build final slice 778 - out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults)) 779 - for _, r := range concurrentResults { 780 - if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil { 781 - out = append(out, r.Value) 782 - } 783 - } 784 - 785 - // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{ 786 - // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"` 787 - // Thread: out, 788 - // HasOtherReplies: false, 789 - // }) 790 - resp := &GetPostThreadOtherV2_Output_WithOtherReplies{ 791 - UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{ 792 - Thread: out, 793 - }, 794 - HasOtherReplies: false, 795 - } 796 - c.JSON(http.StatusOK, resp) 632 + //appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2(c, sl, cs, BSKYIMAGECDN_URL) 633 + // V2V2 still doesnt work. should probably make the handler from scratch to fully use the thread grapher. 634 + // also the thread grapher is still sequental. pls fix that 635 + //appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2V2(c, sl, cs, BSKYIMAGECDN_URL) 636 + appbskyunspeccedgetpostthreadv2.HandleGetPostThreadV2V3(c, sl, cs, BSKYIMAGECDN_URL) 797 637 }) 798 638 799 639 // weird stuff
+1 -1
readme.md
··· 15 15 - `app.bsky.feed.getPosts` (post rendering is incomplete) 16 16 - `app.bsky.feed.getFeed` (post rendering is incomplete) 17 17 - `app.bsky.unspecced.getConfig` (placeholder) 18 - - `app.bsky.unspecced.getPostThreadV2` (thread rendering is incomplete) 18 + - `app.bsky.unspecced.getPostThreadV2` (mostly working! doesnt use prefered sort, not performant)
+1 -2
shims/lex/app/bsky/actor/defs/profileview.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 - "log" 7 6 8 7 "github.com/bluesky-social/indigo/api/agnostic" 9 8 appbsky "github.com/bluesky-social/indigo/api/bsky" ··· 91 90 //viewerProfileURI, err_viewerProfileURI := syntax.ParseATURI("at://" + viewerProfileDID + "/app.bsky.actor.profile/self") 92 91 //targetProfileURI, err_targetProfileURI := syntax.ParseATURI("at://" + targetProfileDID + "/app.bsky.actor.profile/self") 93 92 94 - log.Println("viewerProfileDID: " + viewerProfileDID + " and targetProfileDID: " + targetProfileDID) 93 + //log.Println("viewerProfileDID: " + viewerProfileDID + " and targetProfileDID: " + targetProfileDID) 95 94 96 95 if viewerProfileDID != "" && targetProfileDID != "" { 97 96 blockingBacklink, err_blockingBacklink := constellation.GetBacklinks(ctx, cs, targetProfileDID, "app.bsky.graph.block:subject", []string{viewerProfileDID}, nil, nil)
+525
shims/lex/app/bsky/unspecced/getpostthreadv2/query.go
··· 1 + package appbskyunspeccedgetpostthreadv2 2 + 3 + import ( 4 + "net/http" 5 + "strconv" 6 + "sync" 7 + 8 + "context" 9 + "fmt" 10 + "log" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "github.com/gin-gonic/gin" 14 + "tangled.org/whey.party/red-dwarf-server/microcosm" 15 + "tangled.org/whey.party/red-dwarf-server/shims/utils" 16 + 17 + "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 18 + appbskyfeeddefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/feed/defs" 19 + 20 + appbsky "github.com/bluesky-social/indigo/api/bsky" 21 + ) 22 + 23 + func HandleGetPostThreadV2(c *gin.Context, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string) { 24 + ctx := c.Request.Context() 25 + 26 + rawdid := c.GetString("user_did") 27 + var viewer *utils.DID 28 + didval, errdid := utils.NewDID(rawdid) 29 + if errdid != nil { 30 + viewer = nil 31 + } else { 32 + viewer = &didval 33 + } 34 + 35 + threadAnchorURIraw := c.Query("anchor") 36 + if threadAnchorURIraw == "" { 37 + c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 38 + return 39 + } 40 + 41 + threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 42 + if err != nil { 43 + return 44 + } 45 + 46 + //var thread []*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem 47 + 48 + var skeletonposts []string 49 + 50 + emptystrarray := &[]string{} 51 + limit := 100 52 + 53 + // recurse to the top 54 + parentsMap := map[string]*appbsky.FeedDefs_PostView{} 55 + current := threadAnchorURI 56 + iteration := 0 57 + for true { 58 + iteration = iteration + 1 59 + if iteration > 10 { 60 + break 61 + } 62 + recordaturi, err := syntax.ParseATURI("at://" + current.Authority().String() + "/" + string(current.Collection()) + "/" + string(current.RecordKey())) 63 + if err != nil { 64 + break 65 + } 66 + // loop 67 + postView, postRecord, err := appbskyfeeddefs.PostView(ctx, recordaturi.String(), sl, cs, imgcdn, viewer, 3) 68 + if err != nil { 69 + break 70 + } 71 + if postView == nil { 72 + break 73 + } 74 + //if iteration != 1 { 75 + skeletonposts = append([]string{recordaturi.String()}, skeletonposts...) 76 + //} 77 + parentsMap[recordaturi.String()] = postView 78 + if postRecord != nil && postRecord.Reply != nil && postRecord.Reply.Parent != nil && postRecord.Reply.Root != nil { 79 + if postRecord.Reply.Parent.Uri == postRecord.Reply.Root.Uri { 80 + //break 81 + iteration = 9 82 + } 83 + 84 + current, err = syntax.ParseATURI(postRecord.Reply.Parent.Uri) 85 + if err != nil { 86 + break 87 + } 88 + } else { 89 + log.Println("what huh what hwat") 90 + break //whatt 91 + } 92 + 93 + } 94 + 95 + //skeletonposts = append(skeletonposts, threadAnchorURI.String()) 96 + // todo: theres a cursor!!! pagination please! 97 + // todo: also i doubt im gonna do proper threadding right now, so make sure to remind me to do it properly some time later thanks 98 + //rootReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, nil) 99 + parentReplies, _ := constellation.GetBacklinks(ctx, cs, string(threadAnchorURI), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, nil) 100 + 101 + for _, rec := range parentReplies.Records { 102 + recordaturi, err := syntax.ParseATURI("at://" + rec.Did + "/" + rec.Collection + "/" + rec.Rkey) 103 + if err != nil { 104 + continue 105 + } 106 + skeletonposts = append(skeletonposts, recordaturi.String()) 107 + } 108 + maplen := len(parentsMap) 109 + concurrentResults := MapConcurrent( 110 + ctx, 111 + skeletonposts, 112 + 20, 113 + func(ctx context.Context, raw string, idx int) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) { 114 + var postView *appbsky.FeedDefs_PostView 115 + fromParentChain := false 116 + postView, ok := parentsMap[raw] 117 + if !ok { 118 + post, _, err := appbskyfeeddefs.PostView(ctx, raw, sl, cs, imgcdn, viewer, 3) 119 + if err != nil { 120 + return nil, err 121 + } 122 + if post == nil { 123 + return nil, fmt.Errorf("post not found") 124 + } 125 + postView = post 126 + } else { 127 + fromParentChain = true 128 + } 129 + 130 + depth := int64(1) 131 + if raw == threadAnchorURI.String() { 132 + depth = 0 133 + } 134 + if fromParentChain { 135 + depth = int64(0 - maplen + idx + 1) 136 + } 137 + 138 + return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{ 139 + // Depth int64 `json:"depth" cborgen:"depth"` 140 + Depth: depth, // todo: placeholder 141 + // Uri string `json:"uri" cborgen:"uri"` 142 + Uri: raw, 143 + // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"` 144 + Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{ 145 + // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost 146 + UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{ 147 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"` 148 + LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost", 149 + // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. 150 + // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"` 151 + HiddenByThreadgate: false, // todo: placeholder 152 + // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents. 153 + // MoreParents bool `json:"moreParents" cborgen:"moreParents"` 154 + MoreParents: false, // todo: placeholder 155 + // // 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. 156 + // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"` 157 + MoreReplies: 0, // todo: placeholder 158 + // // mutedByViewer: This is by an account muted by the viewer requesting it. 159 + // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"` 160 + MutedByViewer: false, // todo: placeholder 161 + // // 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. 162 + // OpThread bool `json:"opThread" cborgen:"opThread"` 163 + OpThread: false, // todo: placeholder 164 + // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 165 + Post: postView, 166 + }, 167 + }, 168 + }, nil 169 + }, 170 + ) 171 + 172 + // build final slice 173 + out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults)) 174 + for _, r := range concurrentResults { 175 + if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil { 176 + out = append(out, r.Value) 177 + } 178 + } 179 + 180 + // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{ 181 + // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"` 182 + // Thread: out, 183 + // HasOtherReplies: false, 184 + // }) 185 + resp := &GetPostThreadOtherV2_Output_WithOtherReplies{ 186 + UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{ 187 + Thread: out, 188 + }, 189 + HasOtherReplies: false, 190 + } 191 + c.JSON(http.StatusOK, resp) 192 + } 193 + 194 + type GetPostThreadOtherV2_Output_WithOtherReplies struct { 195 + appbsky.UnspeccedGetPostThreadOtherV2_Output 196 + HasOtherReplies bool `json:"hasOtherReplies"` 197 + } 198 + 199 + type AsyncResult[T any] struct { 200 + Value T 201 + Err error 202 + } 203 + 204 + func MapConcurrent[T any, R any]( 205 + ctx context.Context, 206 + items []T, 207 + concurrencyLimit int, 208 + mapper func(context.Context, T, int) (R, error), 209 + ) []AsyncResult[R] { 210 + if len(items) == 0 { 211 + return nil 212 + } 213 + 214 + results := make([]AsyncResult[R], len(items)) 215 + var wg sync.WaitGroup 216 + 217 + sem := make(chan struct{}, concurrencyLimit) 218 + 219 + for i, item := range items { 220 + wg.Add(1) 221 + go func(idx int, input T) { 222 + defer wg.Done() 223 + 224 + sem <- struct{}{} 225 + defer func() { <-sem }() 226 + 227 + if ctx.Err() != nil { 228 + results[idx] = AsyncResult[R]{Err: ctx.Err()} 229 + return 230 + } 231 + 232 + val, err := mapper(ctx, input, idx) 233 + results[idx] = AsyncResult[R]{Value: val, Err: err} 234 + }(i, item) 235 + } 236 + 237 + wg.Wait() 238 + return results 239 + } 240 + 241 + type SkeletonPost struct { 242 + post syntax.ATURI 243 + depth int 244 + } 245 + 246 + func HandleGetPostThreadV2V3(c *gin.Context, sl *microcosm.MicrocosmClient, cs *microcosm.MicrocosmClient, imgcdn string) { 247 + ctx := c.Request.Context() 248 + 249 + rawdid := c.GetString("user_did") 250 + var viewer *utils.DID 251 + didval, errdid := utils.NewDID(rawdid) 252 + if errdid != nil { 253 + viewer = nil 254 + } else { 255 + viewer = &didval 256 + } 257 + 258 + threadAnchorURIraw := c.Query("anchor") 259 + if threadAnchorURIraw == "" { 260 + c.JSON(http.StatusBadRequest, gin.H{"error": "Missing feed param"}) 261 + return 262 + } 263 + 264 + // "Whether to include parents above the anchor. 265 + // bool as string 266 + // true as default 267 + aboveParam := c.Query("above") // why would you need above = false ? 268 + above := true 269 + if aboveParam == "false" { 270 + above = false 271 + } 272 + 273 + // "How many levels of replies to include below the anchor." 274 + // integer as string 275 + // default: 6, min: 0, max: 20 276 + belowParam := c.Query("below") // bskydefault: 10 277 + below, err_below := strconv.ParseInt(belowParam, 10, 64) 278 + if err_below != nil { 279 + below = 6 280 + } else { 281 + if below > 20 { 282 + below = 20 283 + } 284 + if below < 0 { 285 + below = 0 286 + } 287 + } 288 + 289 + // "Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, 290 + // which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated)." 291 + // integer as string 292 + // default: 10, min: 0, max: 100 293 + branchingFactorParam := c.Query("branchingFactor") // bskydefault: 1 294 + branchingFactor, err_branchingFactor := strconv.ParseInt(branchingFactorParam, 10, 64) 295 + if err_branchingFactor != nil { 296 + branchingFactor = 10 297 + } else { 298 + if branchingFactor > 100 { 299 + branchingFactor = 100 300 + } 301 + if branchingFactor < 0 { 302 + branchingFactor = 0 303 + } 304 + } 305 + 306 + // "Sorting for the thread replies" 307 + // string (enum) ["newest", "oldest", "top"] 308 + // default: "oldest" 309 + sortParam := c.Query("sort") // bskydefault: top 310 + sort := sortParam 311 + if sort != "newest" && sort != "oldest" && sort != "top" { 312 + sort = "top" 313 + } 314 + 315 + threadAnchorURI, err := syntax.ParseATURI(threadAnchorURIraw) 316 + if err != nil { 317 + return 318 + } 319 + 320 + threadGraph, err := ThreadGrapher(ctx, cs, sl, threadAnchorURI) 321 + if err != nil { 322 + c.JSON(http.StatusBadRequest, gin.H{"error": ("failed to graph the thread: " + err.Error())}) 323 + return 324 + } 325 + var skeletonposts []SkeletonPost 326 + 327 + // Parent Chain 328 + parentChainHeight := 0 329 + 330 + // root := threadGraph.RootURI 331 + if above { 332 + current := threadAnchorURI 333 + 334 + for true { 335 + log.Println("[parent threader] current: " + current.Authority().String() + string(current.Collection()) + string(current.RecordKey())) 336 + if parentChainHeight > 20 { 337 + // root = current 338 + break 339 + } 340 + parent, ok := threadGraph.ParentsMap[current] 341 + if !ok { 342 + // root = current 343 + break 344 + } 345 + parentChainHeight = parentChainHeight + 1 346 + skeletonposts = append(skeletonposts, SkeletonPost{ 347 + post: parent, 348 + depth: -parentChainHeight, 349 + }) 350 + current = parent 351 + } 352 + flipArray(&skeletonposts) 353 + } 354 + 355 + // handled by the recurser 356 + // // Anchor Post 357 + // skeletonposts = append(skeletonposts, SkeletonPost{ 358 + // post: threadAnchorURI, 359 + // depth: 0, 360 + // }) 361 + 362 + // Tree Replies (with OP thread priority) 363 + // should probably be recursive 364 + recursiveHandleV2V3TreeReplies(threadGraph, &skeletonposts, threadAnchorURI, &below, &branchingFactor, &sort, 0, 0) 365 + 366 + //maplen := len(parentsMap) 367 + concurrentResults := MapConcurrent( 368 + ctx, 369 + skeletonposts, 370 + 20, 371 + func(ctx context.Context, raw SkeletonPost, idx int) (*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, error) { 372 + var postView *appbsky.FeedDefs_PostView 373 + //fromParentChain := false 374 + //postView, ok := parentsMap[raw] 375 + //if !ok { 376 + post, _, err := appbskyfeeddefs.PostView(ctx, raw.post.String(), sl, cs, imgcdn, viewer, 3) 377 + if err != nil { 378 + return nil, err 379 + } 380 + if post == nil { 381 + return nil, fmt.Errorf("post not found") 382 + } 383 + postView = post 384 + //} else { 385 + // fromParentChain = true 386 + //} 387 + 388 + depth := int64(1) 389 + // if raw == threadAnchorURI.String() { 390 + // depth = 0 391 + // } 392 + // if fromParentChain { 393 + // depth = int64(0 - parentChainHeight + idx + 1) 394 + // } 395 + depth = int64(raw.depth) 396 + 397 + return &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem{ 398 + // Depth int64 `json:"depth" cborgen:"depth"` 399 + Depth: depth, // todo: placeholder 400 + // Uri string `json:"uri" cborgen:"uri"` 401 + Uri: raw.post.String(), 402 + // Value *UnspeccedGetPostThreadOtherV2_ThreadItem_Value `json:"value" cborgen:"value"` 403 + Value: &appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem_Value{ 404 + // UnspeccedDefs_ThreadItemPost *UnspeccedDefs_ThreadItemPost 405 + UnspeccedDefs_ThreadItemPost: &appbsky.UnspeccedDefs_ThreadItemPost{ 406 + // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.unspecced.defs#threadItemPost"` 407 + LexiconTypeID: "app.bsky.unspecced.defs#threadItemPost", 408 + // // hiddenByThreadgate: The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread. 409 + // HiddenByThreadgate bool `json:"hiddenByThreadgate" cborgen:"hiddenByThreadgate"` 410 + HiddenByThreadgate: false, // todo: placeholder 411 + // // moreParents: This post has more parents that were not present in the response. This is just a boolean, without the number of parents. 412 + // MoreParents bool `json:"moreParents" cborgen:"moreParents"` 413 + MoreParents: false, // todo: placeholder 414 + // // 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. 415 + // MoreReplies int64 `json:"moreReplies" cborgen:"moreReplies"` 416 + MoreReplies: 0, // todo: placeholder 417 + // // mutedByViewer: This is by an account muted by the viewer requesting it. 418 + // MutedByViewer bool `json:"mutedByViewer" cborgen:"mutedByViewer"` 419 + MutedByViewer: false, // todo: placeholder 420 + // // 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. 421 + // OpThread bool `json:"opThread" cborgen:"opThread"` 422 + OpThread: false, // todo: placeholder 423 + // Post *FeedDefs_PostView `json:"post" cborgen:"post"` 424 + Post: postView, 425 + }, 426 + }, 427 + }, nil 428 + }, 429 + ) 430 + 431 + // build final slice 432 + out := make([]*appbsky.UnspeccedGetPostThreadOtherV2_ThreadItem, 0, len(concurrentResults)) 433 + for _, r := range concurrentResults { 434 + if r.Err == nil && r.Value != nil && r.Value.Value != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost != nil && r.Value.Value.UnspeccedDefs_ThreadItemPost.Post != nil { 435 + out = append(out, r.Value) 436 + } 437 + } 438 + 439 + // c.JSON(http.StatusOK, &appbsky.UnspeccedGetPostThreadOtherV2_Output{ 440 + // // Thread []*UnspeccedGetPostThreadOtherV2_ThreadItem `json:"thread" cborgen:"thread"` 441 + // Thread: out, 442 + // HasOtherReplies: false, 443 + // }) 444 + resp := &GetPostThreadOtherV2_Output_WithOtherReplies{ 445 + UnspeccedGetPostThreadOtherV2_Output: appbsky.UnspeccedGetPostThreadOtherV2_Output{ 446 + Thread: out, 447 + }, 448 + HasOtherReplies: false, 449 + } 450 + c.JSON(http.StatusOK, resp) 451 + } 452 + 453 + func flipArray[T any](s *[]T) { 454 + for i, j := 0, len(*s)-1; i < j; i, j = i+1, j-1 { 455 + (*s)[i], (*s)[j] = (*s)[j], (*s)[i] 456 + } 457 + } 458 + 459 + func recursiveHandleV2V3TreeReplies(threadGraph *ThreadGraph, skeletonposts *[]SkeletonPost, current syntax.ATURI, below *int64, branchingFactor *int64, sort *string, verticalPos int64, horizontalPos int64) { 460 + log.Println("[V3 Recurse] at y: " + fmt.Sprint(verticalPos) + ", x: " + fmt.Sprint(horizontalPos)) 461 + // breakings 462 + if below != nil && verticalPos > *below { 463 + log.Println("[V3 Recurse] exit by too low") 464 + return 465 + } 466 + if branchingFactor != nil && horizontalPos > *branchingFactor && verticalPos > 1 { 467 + log.Println("[V3 Recurse] exit by too wide; branchingFactor: " + fmt.Sprint(*branchingFactor) + "; horizontalPos: " + fmt.Sprint(horizontalPos) + "; verticalPos: " + fmt.Sprint(verticalPos)) 468 + return 469 + } 470 + 471 + // the things to do if not recurse 472 + if skeletonposts != nil { 473 + *skeletonposts = append(*skeletonposts, SkeletonPost{ 474 + post: current, 475 + depth: int(verticalPos), 476 + }) 477 + } else { 478 + log.Println("[V3 Recurse] exit by no skeleton posts") 479 + return 480 + } 481 + 482 + repliesAtThisPosition, ok := threadGraph.ChildrenMap[current] 483 + if !ok { 484 + log.Println("[V3 Recurse] exit by no replies") 485 + return 486 + } 487 + 488 + // recurse 489 + op := threadGraph.RootURI.Authority() 490 + 491 + // We need a modifyable copy of the slice because we are about to reorder it 492 + // and we don't want to mess up the original map in case it's used elsewhere. 493 + sortedReplies := make([]syntax.ATURI, len(repliesAtThisPosition)) 494 + copy(sortedReplies, repliesAtThisPosition) 495 + 496 + // 1. Find the "best" OP reply (lexicographically smallest rkey usually means oldest) 497 + bestOpIndex := -1 498 + var bestOpURI syntax.ATURI 499 + 500 + for i, replyURI := range sortedReplies { 501 + if replyURI.Authority() == op { 502 + // If this is the first OP reply we found, or if it's lexicographically smaller (older) than the previous best 503 + if bestOpIndex == -1 || replyURI.String() < bestOpURI.String() { 504 + bestOpIndex = i 505 + bestOpURI = replyURI 506 + } 507 + } 508 + } 509 + 510 + // 2. If we found an OP reply, move it to index 0 511 + if bestOpIndex > 0 { // If it's already 0, no need to move 512 + // Remove the best OP reply from its current spot 513 + // (Go slice trick: append everything before it + everything after it) 514 + withoutBest := append(sortedReplies[:bestOpIndex], sortedReplies[bestOpIndex+1:]...) 515 + 516 + // Prepend it to the front 517 + sortedReplies = append([]syntax.ATURI{bestOpURI}, withoutBest...) 518 + } 519 + 520 + for idx, reply := range sortedReplies { 521 + recursiveHandleV2V3TreeReplies(threadGraph, skeletonposts, reply, below, branchingFactor, sort, verticalPos+1, int64(idx+1)) 522 + } 523 + 524 + // going up 525 + }
+145
shims/lex/app/bsky/unspecced/getpostthreadv2/threadgrapher.go
··· 1 + package appbskyunspeccedgetpostthreadv2 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "log" 8 + "sync" 9 + 10 + "github.com/bluesky-social/indigo/api/agnostic" 11 + //comatproto "github.com/bluesky-social/indigo/api/atproto" 12 + appbsky "github.com/bluesky-social/indigo/api/bsky" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + 15 + "tangled.org/whey.party/red-dwarf-server/microcosm" 16 + "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 17 + ) 18 + 19 + type ThreadGraph struct { 20 + mu sync.RWMutex // Mutex required for thread-safe updates (Firehose) 21 + 22 + RootURI syntax.ATURI 23 + AnchorURI syntax.ATURI 24 + 25 + ParentsMap map[syntax.ATURI]syntax.ATURI 26 + 27 + ChildrenMap map[syntax.ATURI][]syntax.ATURI 28 + } 29 + 30 + func NewThreadGraph() *ThreadGraph { 31 + return &ThreadGraph{ 32 + ParentsMap: make(map[syntax.ATURI]syntax.ATURI), 33 + ChildrenMap: make(map[syntax.ATURI][]syntax.ATURI), 34 + } 35 + } 36 + 37 + func ThreadGrapher(ctx context.Context, cs *microcosm.MicrocosmClient, sl *microcosm.MicrocosmClient, threadAnchorURI syntax.ATURI) (*ThreadGraph, error) { 38 + tg := NewThreadGraph() 39 + tg.AnchorURI = threadAnchorURI 40 + 41 + anchorPostRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.post", threadAnchorURI.Authority().String(), string(threadAnchorURI.RecordKey())) 42 + if err != nil { 43 + log.Println("[ThreadGrapher] exit by anchor post resolve failure") 44 + return nil, fmt.Errorf("failed to fetch anchor: %w", err) 45 + } 46 + 47 + var anchorPost appbsky.FeedPost 48 + if err := json.Unmarshal(*anchorPostRecordResponse.Value, &anchorPost); err != nil { 49 + log.Println("[ThreadGrapher] exit by no json") 50 + return nil, fmt.Errorf("error: Failed to parse post record JSON") 51 + } 52 + 53 + var rootURI syntax.ATURI 54 + 55 + if anchorPost.Reply != nil && anchorPost.Reply.Root != nil { 56 + rURI, err := syntax.ParseATURI(anchorPost.Reply.Root.Uri) 57 + if err != nil { 58 + log.Println("[ThreadGrapher] exit by invalid root uri") 59 + return nil, fmt.Errorf("invalid root uri in record: %w", err) 60 + } 61 + rootURI = rURI 62 + 63 + // todo: fiine we wont fetch the root post, but we still need to mark it as deleted somehow 64 + // rootPostRecordResponse, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.feed.post", rootURI.Authority().String(), string(rootURI.RecordKey())) 65 + // if err != nil { 66 + // log.Println("[ThreadGrapher] exit by cant fetch root") 67 + // return nil, fmt.Errorf("failed to fetch root: %w", err) 68 + // } 69 + 70 + // var rootPost appbsky.FeedPost 71 + // if err := json.Unmarshal(*rootPostRecordResponse.Value, &rootPost); err != nil { 72 + // log.Println("[ThreadGrapher] exit by cant parse root json") 73 + // return nil, fmt.Errorf("error: Failed to parse post record JSON") 74 + // } 75 + // if err == nil { 76 + // tg.AddNode(rootURI, &rootPost) 77 + // } 78 + } else { 79 + rootURI = threadAnchorURI 80 + } 81 + tg.RootURI = rootURI 82 + 83 + emptystrarray := &[]string{} 84 + var allRepliesATURI []syntax.ATURI 85 + limit := 100 86 + var cursor *string 87 + shouldContinue := true 88 + for shouldContinue { 89 + results, err := constellation.GetBacklinks(ctx, cs, rootURI.String(), "app.bsky.feed.post:reply.root.uri", *emptystrarray, &limit, cursor) 90 + if err != nil { 91 + log.Println("[ThreadGrapher] [root graphing] exit by backlink failure") 92 + return nil, fmt.Errorf("failed to get backlinks: %w", err) 93 + } 94 + if results.Records != nil { 95 + for _, record := range results.Records { 96 + aturi, err := syntax.ParseATURI("at://" + record.Did + "/" + record.Collection + "/" + record.Rkey) 97 + if err == nil { 98 + allRepliesATURI = append(allRepliesATURI, aturi) 99 + } 100 + } 101 + } 102 + if results.Cursor != nil { 103 + cursor = results.Cursor 104 + } else { 105 + shouldContinue = false 106 + } 107 + } 108 + 109 + processingQueue := append([]syntax.ATURI{rootURI}, allRepliesATURI...) 110 + for _, aturi := range processingQueue { 111 + //tg.Nodes = append(tg.Nodes, aturi) 112 + // graphinger 113 + emptystrarray := &[]string{} 114 + var localRepliesATURI []syntax.ATURI 115 + limit := 100 116 + var cursor *string 117 + shouldContinue := true 118 + for shouldContinue { 119 + results, err := constellation.GetBacklinks(ctx, cs, aturi.String(), "app.bsky.feed.post:reply.parent.uri", *emptystrarray, &limit, cursor) 120 + if err != nil { 121 + log.Println("[ThreadGrapher] [parent graphing] exit by no replies") 122 + return nil, fmt.Errorf("failed to get backlinks: %w", err) 123 + } 124 + if results.Records != nil { 125 + for _, record := range results.Records { 126 + aturi, err := syntax.ParseATURI("at://" + record.Did + "/" + record.Collection + "/" + record.Rkey) 127 + if err == nil { 128 + localRepliesATURI = append(localRepliesATURI, aturi) 129 + } 130 + } 131 + } 132 + if results.Cursor != nil { 133 + cursor = results.Cursor 134 + } else { 135 + shouldContinue = false 136 + } 137 + } 138 + for _, reply := range localRepliesATURI { 139 + tg.ParentsMap[reply] = aturi 140 + tg.ChildrenMap[aturi] = append(tg.ChildrenMap[aturi], reply) 141 + } 142 + } 143 + 144 + return tg, nil 145 + }