A locally focused bluesky appview

better actor hydration

Changed files
+90 -26
hydration
views
xrpc
+1
.gitignore
··· 2 2 frontend/node_modules 3 3 konbini 4 4 sequence.txt 5 + atproto
-1
README.md
··· 19 19 ### Prerequisites 20 20 21 21 - Docker and Docker Compose installed 22 - - Your Bluesky DID (find it at https://bsky.app/settings/account) 23 22 24 23 ### Setup 25 24
+76
hydration/actor.go
··· 6 6 "fmt" 7 7 "log/slog" 8 8 "strings" 9 + "sync" 9 10 10 11 "github.com/bluesky-social/indigo/api/bsky" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" ··· 52 53 } 53 54 54 55 return info, nil 56 + } 57 + 58 + type ActorInfoDetailed struct { 59 + ActorInfo 60 + FollowCount int64 61 + FollowerCount int64 62 + PostCount int64 63 + } 64 + 65 + func (h *Hydrator) HydrateActorDetailed(ctx context.Context, did string) (*ActorInfoDetailed, error) { 66 + act, err := h.HydrateActor(ctx, did) 67 + if err != nil { 68 + return nil, err 69 + } 70 + 71 + actd := ActorInfoDetailed{ 72 + ActorInfo: *act, 73 + } 74 + 75 + var wg sync.WaitGroup 76 + wg.Add(3) 77 + go func() { 78 + defer wg.Done() 79 + c, err := h.getFollowCountForUser(ctx, did) 80 + if err != nil { 81 + slog.Error("failed to get follow count", "did", did, "error", err) 82 + } 83 + actd.FollowCount = c 84 + }() 85 + go func() { 86 + defer wg.Done() 87 + c, err := h.getFollowerCountForUser(ctx, did) 88 + if err != nil { 89 + slog.Error("failed to get follower count", "did", did, "error", err) 90 + } 91 + actd.FollowerCount = c 92 + }() 93 + go func() { 94 + defer wg.Done() 95 + c, err := h.getPostCountForUser(ctx, did) 96 + if err != nil { 97 + slog.Error("failed to get post count", "did", did, "error", err) 98 + } 99 + actd.PostCount = c 100 + }() 101 + wg.Wait() 102 + 103 + return &actd, nil 104 + } 105 + 106 + func (h *Hydrator) getFollowCountForUser(ctx context.Context, did string) (int64, error) { 107 + var count int64 108 + if err := h.db.Raw("SELECT count(*) FROM follows WHERE author = (SELECT id FROM repos WHERE did = ?)", did).Scan(&count).Error; err != nil { 109 + return 0, err 110 + } 111 + 112 + return count, nil 113 + } 114 + 115 + func (h *Hydrator) getFollowerCountForUser(ctx context.Context, did string) (int64, error) { 116 + var count int64 117 + if err := h.db.Raw("SELECT count(*) FROM follows WHERE subject = (SELECT id FROM repos WHERE did = ?)", did).Scan(&count).Error; err != nil { 118 + return 0, err 119 + } 120 + 121 + return count, nil 122 + } 123 + 124 + func (h *Hydrator) getPostCountForUser(ctx context.Context, did string) (int64, error) { 125 + var count int64 126 + if err := h.db.Raw("SELECT count(*) FROM posts WHERE author = (SELECT id FROM repos WHERE did = ?)", did).Scan(&count).Error; err != nil { 127 + return 0, err 128 + } 129 + 130 + return count, nil 55 131 } 56 132 57 133 // HydrateActors hydrates multiple actors
+4 -7
views/actor.go
··· 57 57 } 58 58 59 59 // ProfileViewDetailed builds a detailed profile view (app.bsky.actor.defs#profileViewDetailed) 60 - func ProfileViewDetailed(actor *hydration.ActorInfo, followerCount, followsCount, postsCount int) *bsky.ActorDefs_ProfileViewDetailed { 60 + func ProfileViewDetailed(actor *hydration.ActorInfoDetailed) *bsky.ActorDefs_ProfileViewDetailed { 61 61 view := &bsky.ActorDefs_ProfileViewDetailed{ 62 62 Did: actor.DID, 63 63 Handle: actor.Handle, ··· 85 85 } 86 86 87 87 // Add counts 88 - fc := int64(followerCount) 89 - fsc := int64(followsCount) 90 - pc := int64(postsCount) 91 - view.FollowersCount = &fc 92 - view.FollowsCount = &fsc 93 - view.PostsCount = &pc 88 + view.FollowersCount = &actor.FollowerCount 89 + view.FollowsCount = &actor.FollowCount 90 + view.PostsCount = &actor.PostCount 94 91 95 92 return view 96 93 }
+2 -11
xrpc/actor/getProfile.go
··· 30 30 } 31 31 32 32 // Hydrate actor info 33 - actorInfo, err := hydrator.HydrateActor(ctx, did) 33 + actorInfo, err := hydrator.HydrateActorDetailed(ctx, did) 34 34 if err != nil { 35 35 return c.JSON(http.StatusNotFound, map[string]interface{}{ 36 36 "error": "ActorNotFound", ··· 38 38 }) 39 39 } 40 40 41 - // Get follower/follows/posts counts 42 - // TODO: These queries should be optimized 43 - var followerCount, followsCount, postsCount int 44 - 45 - // We'll return 0 for now - can optimize later 46 - followerCount = 0 47 - followsCount = 0 48 - postsCount = 0 49 - 50 41 // Build response 51 - profile := views.ProfileViewDetailed(actorInfo, followerCount, followsCount, postsCount) 42 + profile := views.ProfileViewDetailed(actorInfo) 52 43 53 44 return c.JSON(http.StatusOK, profile) 54 45 }
+2 -2
xrpc/actor/getProfiles.go
··· 38 38 } 39 39 40 40 // Hydrate actor info 41 - actorInfo, err := hydrator.HydrateActor(ctx, did) 41 + actorInfo, err := hydrator.HydrateActorDetailed(ctx, did) 42 42 if err != nil { 43 43 // Skip actors that can't be hydrated 44 44 continue ··· 58 58 (SELECT COUNT(*) FROM posts WHERE author = (SELECT id FROM repos WHERE did = ?)) as posts 59 59 `, did, did, did).Scan(&c) 60 60 61 - profiles = append(profiles, views.ProfileViewDetailed(actorInfo, c.Followers, c.Follows, c.Posts)) 61 + profiles = append(profiles, views.ProfileViewDetailed(actorInfo)) 62 62 } 63 63 64 64 return c.JSON(http.StatusOK, map[string]interface{}{
+5 -5
xrpc/feed/getPostThread.go
··· 49 49 50 50 // Query all posts in this thread 51 51 type threadPost struct { 52 - ID uint 53 - Rkey string 54 - ReplyTo uint 55 - InThread uint 56 - AuthorDID string 52 + ID uint 53 + Rkey string 54 + ReplyTo uint 55 + InThread uint 56 + AuthorDID string 57 57 } 58 58 var threadPosts []threadPost 59 59 db.Raw(`