A locally focused bluesky appview
at master 2.7 kB view raw
1package graph 2 3import ( 4 "net/http" 5 "strconv" 6 7 "github.com/labstack/echo/v4" 8 "github.com/whyrusleeping/konbini/hydration" 9 "github.com/whyrusleeping/konbini/views" 10 "gorm.io/gorm" 11) 12 13// HandleGetFollowers implements app.bsky.graph.getFollowers 14func HandleGetFollowers(c echo.Context, db *gorm.DB, hydrator *hydration.Hydrator) error { 15 actorParam := c.QueryParam("actor") 16 if actorParam == "" { 17 return c.JSON(http.StatusBadRequest, map[string]interface{}{ 18 "error": "InvalidRequest", 19 "message": "actor parameter is required", 20 }) 21 } 22 23 // Parse limit 24 limit := 50 25 if limitParam := c.QueryParam("limit"); limitParam != "" { 26 if l, err := strconv.Atoi(limitParam); err == nil && l > 0 && l <= 100 { 27 limit = l 28 } 29 } 30 31 // Parse cursor (follow ID) 32 var cursor uint 33 if cursorParam := c.QueryParam("cursor"); cursorParam != "" { 34 if c, err := strconv.ParseUint(cursorParam, 10, 64); err == nil { 35 cursor = uint(c) 36 } 37 } 38 39 ctx := c.Request().Context() 40 41 // Resolve actor to DID 42 did, err := hydrator.ResolveDID(ctx, actorParam) 43 if err != nil { 44 return c.JSON(http.StatusBadRequest, map[string]interface{}{ 45 "error": "ActorNotFound", 46 "message": "actor not found", 47 }) 48 } 49 50 // Get the subject actor info 51 subjectInfo, err := hydrator.HydrateActor(ctx, did) 52 if err != nil { 53 return c.JSON(http.StatusNotFound, map[string]interface{}{ 54 "error": "ActorNotFound", 55 "message": "failed to load actor", 56 }) 57 } 58 59 // Query followers 60 type followerRow struct { 61 ID uint 62 AuthorDid string 63 } 64 var rows []followerRow 65 66 query := ` 67 SELECT f.id, r.did as author_did 68 FROM follows f 69 JOIN repos r ON r.id = f.author 70 WHERE f.subject = (SELECT id FROM repos WHERE did = ?) 71 ` 72 if cursor > 0 { 73 query += ` AND f.id < ?` 74 } 75 query += ` ORDER BY f.id DESC LIMIT ?` 76 77 var queryArgs []interface{} 78 queryArgs = append(queryArgs, did) 79 if cursor > 0 { 80 queryArgs = append(queryArgs, cursor) 81 } 82 queryArgs = append(queryArgs, limit) 83 84 if err := db.Raw(query, queryArgs...).Scan(&rows).Error; err != nil { 85 return c.JSON(http.StatusInternalServerError, map[string]interface{}{ 86 "error": "InternalError", 87 "message": "failed to query followers", 88 }) 89 } 90 91 // Hydrate follower actors 92 followers := make([]interface{}, 0) 93 for _, row := range rows { 94 actorInfo, err := hydrator.HydrateActor(ctx, row.AuthorDid) 95 if err != nil { 96 continue 97 } 98 followers = append(followers, views.ProfileView(actorInfo)) 99 } 100 101 // Generate next cursor 102 var nextCursor string 103 if len(rows) > 0 { 104 nextCursor = strconv.FormatUint(uint64(rows[len(rows)-1].ID), 10) 105 } 106 107 return c.JSON(http.StatusOK, map[string]interface{}{ 108 "subject": views.ProfileView(subjectInfo), 109 "followers": followers, 110 "cursor": nextCursor, 111 }) 112}