A locally focused bluesky appview

viewer state hydration

Changed files
+132 -16
hydration
views
xrpc
+110 -11
hydration/actor.go
··· 10 10 11 11 "github.com/bluesky-social/indigo/api/bsky" 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 + "github.com/whyrusleeping/market/models" 13 14 ) 14 15 15 16 // ActorInfo contains hydrated actor information ··· 60 61 FollowCount int64 61 62 FollowerCount int64 62 63 PostCount int64 64 + ViewerState *bsky.ActorDefs_ViewerState 63 65 } 64 66 65 - func (h *Hydrator) HydrateActorDetailed(ctx context.Context, did string) (*ActorInfoDetailed, error) { 67 + func (h *Hydrator) HydrateActorDetailed(ctx context.Context, did string, viewer string) (*ActorInfoDetailed, error) { 66 68 act, err := h.HydrateActor(ctx, did) 67 69 if err != nil { 68 70 return nil, err ··· 73 75 } 74 76 75 77 var wg sync.WaitGroup 76 - wg.Add(3) 77 - go func() { 78 - defer wg.Done() 78 + wg.Go(func() { 79 79 c, err := h.getFollowCountForUser(ctx, did) 80 80 if err != nil { 81 81 slog.Error("failed to get follow count", "did", did, "error", err) 82 82 } 83 83 actd.FollowCount = c 84 - }() 85 - go func() { 86 - defer wg.Done() 84 + }) 85 + wg.Go(func() { 87 86 c, err := h.getFollowerCountForUser(ctx, did) 88 87 if err != nil { 89 88 slog.Error("failed to get follower count", "did", did, "error", err) 90 89 } 91 90 actd.FollowerCount = c 92 - }() 93 - go func() { 94 - defer wg.Done() 91 + }) 92 + wg.Go(func() { 95 93 c, err := h.getPostCountForUser(ctx, did) 96 94 if err != nil { 97 95 slog.Error("failed to get post count", "did", did, "error", err) 98 96 } 99 97 actd.PostCount = c 100 - }() 98 + }) 99 + 100 + if viewer != "" { 101 + wg.Go(func() { 102 + vs, err := h.getProfileViewerState(ctx, did, viewer) 103 + if err != nil { 104 + slog.Error("failed to get viewer state", "did", did, "viewer", viewer, "error", err) 105 + } 106 + actd.ViewerState = vs 107 + }) 108 + } 109 + 101 110 wg.Wait() 102 111 103 112 return &actd, nil 113 + } 114 + 115 + func (h *Hydrator) getProfileViewerState(ctx context.Context, did, viewer string) (*bsky.ActorDefs_ViewerState, error) { 116 + vs := &bsky.ActorDefs_ViewerState{} 117 + 118 + var wg sync.WaitGroup 119 + 120 + // Check if viewer is blocked by the target account 121 + wg.Go(func() { 122 + blockedBy, err := h.getBlockPair(ctx, did, viewer) 123 + if err != nil { 124 + slog.Error("failed to get blockedBy relationship", "did", did, "viewer", viewer, "error", err) 125 + return 126 + } 127 + 128 + if blockedBy != nil { 129 + v := true 130 + vs.BlockedBy = &v 131 + } 132 + }) 133 + 134 + // Check if viewer is blocking the target account 135 + wg.Go(func() { 136 + blocking, err := h.getBlockPair(ctx, viewer, did) 137 + if err != nil { 138 + slog.Error("failed to get blocking relationship", "did", did, "viewer", viewer, "error", err) 139 + return 140 + } 141 + 142 + if blocking != nil { 143 + uri := fmt.Sprintf("at://%s/app.bsky.graph.block/%s", viewer, blocking.Rkey) 144 + vs.Blocking = &uri 145 + } 146 + }) 147 + 148 + // Check if viewer is following the target account 149 + wg.Go(func() { 150 + following, err := h.getFollowPair(ctx, viewer, did) 151 + if err != nil { 152 + slog.Error("failed to get following relationship", "did", did, "viewer", viewer, "error", err) 153 + return 154 + } 155 + 156 + if following != nil { 157 + uri := fmt.Sprintf("at://%s/app.bsky.graph.follow/%s", viewer, following.Rkey) 158 + vs.Following = &uri 159 + } 160 + }) 161 + 162 + // Check if target account is following the viewer 163 + wg.Go(func() { 164 + followedBy, err := h.getFollowPair(ctx, did, viewer) 165 + if err != nil { 166 + slog.Error("failed to get followedBy relationship", "did", did, "viewer", viewer, "error", err) 167 + return 168 + } 169 + 170 + if followedBy != nil { 171 + uri := fmt.Sprintf("at://%s/app.bsky.graph.follow/%s", did, followedBy.Rkey) 172 + vs.FollowedBy = &uri 173 + } 174 + }) 175 + 176 + wg.Wait() 177 + 178 + return vs, nil 179 + } 180 + 181 + func (h *Hydrator) getBlockPair(ctx context.Context, a, b string) (*models.Block, error) { 182 + var blk models.Block 183 + if err := h.db.Raw("SELECT * FROM blocks WHERE author = (SELECT id FROM repos WHERE did = ?) AND subject = (SELECT id FROM repos WHERE did = ?)", a, b).Scan(&blk).Error; err != nil { 184 + return nil, err 185 + } 186 + if blk.ID == 0 { 187 + return nil, nil 188 + } 189 + 190 + return &blk, nil 191 + } 192 + 193 + func (h *Hydrator) getFollowPair(ctx context.Context, a, b string) (*models.Follow, error) { 194 + var fol models.Follow 195 + if err := h.db.Raw("SELECT * FROM follows WHERE author = (SELECT id FROM repos WHERE did = ?) AND subject = (SELECT id FROM repos WHERE did = ?)", a, b).Scan(&fol).Error; err != nil { 196 + return nil, err 197 + } 198 + if fol.ID == 0 { 199 + return nil, nil 200 + } 201 + 202 + return &fol, nil 104 203 } 105 204 106 205 func (h *Hydrator) getFollowCountForUser(ctx context.Context, did string) (int64, error) {
+9
pgbackend.go
··· 21 21 . "github.com/whyrusleeping/konbini/models" 22 22 ) 23 23 24 + func (b *PostgresBackend) DidToID(ctx context.Context, did string) (uint, error) { 25 + r, err := b.getOrCreateRepo(ctx, did) 26 + if err != nil { 27 + return 0, err 28 + } 29 + 30 + return r.ID, nil 31 + } 32 + 24 33 func (b *PostgresBackend) getOrCreateRepo(ctx context.Context, did string) (*Repo, error) { 25 34 r, ok := b.repoCache.Get(did) 26 35 if !ok {
+5
views/actor.go
··· 89 89 view.FollowsCount = &actor.FollowCount 90 90 view.PostsCount = &actor.PostCount 91 91 92 + // Add viewer state if available 93 + if actor.ViewerState != nil { 94 + view.Viewer = actor.ViewerState 95 + } 96 + 92 97 return view 93 98 } 94 99
+3 -1
xrpc/actor/getProfile.go
··· 20 20 21 21 ctx := c.Request().Context() 22 22 23 + viewer, _ := c.Get("viewer").(string) 24 + 23 25 // Resolve actor to DID 24 26 did, err := hydrator.ResolveDID(ctx, actorParam) 25 27 if err != nil { ··· 30 32 } 31 33 32 34 // Hydrate actor info 33 - actorInfo, err := hydrator.HydrateActorDetailed(ctx, did) 35 + actorInfo, err := hydrator.HydrateActorDetailed(ctx, did, viewer) 34 36 if err != nil { 35 37 return c.JSON(http.StatusNotFound, map[string]interface{}{ 36 38 "error": "ActorNotFound",
+2 -1
xrpc/actor/getProfiles.go
··· 27 27 } 28 28 29 29 ctx := c.Request().Context() 30 + viewer, _ := c.Get("viewer").(string) 30 31 31 32 // Resolve all actors to DIDs and hydrate profiles 32 33 profiles := make([]*bsky.ActorDefs_ProfileViewDetailed, 0, len(actors)) ··· 39 40 } 40 41 41 42 // Hydrate actor info 42 - actorInfo, err := hydrator.HydrateActorDetailed(ctx, did) 43 + actorInfo, err := hydrator.HydrateActorDetailed(ctx, did, viewer) 43 44 if err != nil { 44 45 // Skip actors that can't be hydrated 45 46 continue
+3 -3
xrpc/server.go
··· 94 94 // app.bsky.actor.* 95 95 xrpcGroup.GET("/app.bsky.actor.getProfile", func(c echo.Context) error { 96 96 return actor.HandleGetProfile(c, s.hydrator) 97 - }) 97 + }, s.optionalAuth) 98 98 xrpcGroup.GET("/app.bsky.actor.getProfiles", func(c echo.Context) error { 99 99 return actor.HandleGetProfiles(c, s.db, s.hydrator) 100 - }) 100 + }, s.optionalAuth) 101 101 xrpcGroup.GET("/app.bsky.actor.getPreferences", func(c echo.Context) error { 102 102 return actor.HandleGetPreferences(c, s.db, s.hydrator) 103 103 }, s.requireAuth) ··· 131 131 }, s.requireAuth) 132 132 xrpcGroup.GET("/app.bsky.feed.getFeed", func(c echo.Context) error { 133 133 return feed.HandleGetFeed(c, s.db, s.hydrator, s.dir) 134 - }) 134 + }, s.optionalAuth) 135 135 xrpcGroup.GET("/app.bsky.feed.getFeedGenerator", func(c echo.Context) error { 136 136 return feed.HandleGetFeedGenerator(c, s.db, s.hydrator, s.dir) 137 137 })