A locally focused bluesky appview
26
fork

Configure Feed

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

at master 114 lines 2.9 kB view raw
1package feed 2 3import ( 4 "context" 5 "net/http" 6 "strconv" 7 "time" 8 9 "github.com/labstack/echo/v4" 10 "github.com/whyrusleeping/konbini/hydration" 11 "go.opentelemetry.io/otel" 12 "gorm.io/gorm" 13) 14 15var tracer = otel.Tracer("xrpc/feed") 16 17// HandleGetTimeline implements app.bsky.feed.getTimeline 18func HandleGetTimeline(c echo.Context, db *gorm.DB, hydrator *hydration.Hydrator) error { 19 ctx := c.Request().Context() 20 ctx, span := tracer.Start(ctx, "getTimeline") 21 defer span.End() 22 23 viewer := getUserDID(c) 24 if viewer == "" { 25 return c.JSON(http.StatusUnauthorized, map[string]any{ 26 "error": "AuthenticationRequired", 27 "message": "authentication required", 28 }) 29 } 30 31 // Parse limit 32 limit := 50 33 if limitParam := c.QueryParam("limit"); limitParam != "" { 34 if l, err := strconv.Atoi(limitParam); err == nil && l > 0 && l <= 100 { 35 limit = l 36 } 37 } 38 39 // Parse cursor (timestamp) 40 cursor := time.Now() 41 if cursorParam := c.QueryParam("cursor"); cursorParam != "" { 42 if t, err := time.Parse(time.RFC3339, cursorParam); err == nil { 43 cursor = t 44 } 45 } 46 47 // Get viewer's repo ID 48 var viewerRepoID uint 49 if err := db.Raw("SELECT id FROM repos WHERE did = ?", viewer).Scan(&viewerRepoID).Error; err != nil { 50 return c.JSON(http.StatusInternalServerError, map[string]any{ 51 "error": "InternalError", 52 "message": "failed to load viewer", 53 }) 54 } 55 56 // Query posts from followed users 57 58 rows, err := getTimelinePosts(ctx, db, viewerRepoID, cursor, limit) 59 if err != nil { 60 return c.JSON(http.StatusInternalServerError, map[string]any{ 61 "error": "InternalError", 62 "message": "failed to query timeline", 63 }) 64 } 65 66 // Hydrate posts 67 feed := hydratePostRows(ctx, hydrator, viewer, rows) 68 69 // Generate next cursor 70 var nextCursor string 71 if len(rows) > 0 { 72 // Get the created time of the last post 73 var lastCreated time.Time 74 lastURI := rows[len(rows)-1].URI 75 postInfo, err := hydrator.HydratePost(ctx, lastURI, viewer) 76 if err == nil && postInfo.Post != nil { 77 t, err := time.Parse(time.RFC3339, postInfo.Post.CreatedAt) 78 if err == nil { 79 lastCreated = t 80 nextCursor = lastCreated.Format(time.RFC3339) 81 } 82 } 83 } 84 85 return c.JSON(http.StatusOK, map[string]any{ 86 "feed": feed, 87 "cursor": nextCursor, 88 }) 89} 90 91func getTimelinePosts(ctx context.Context, db *gorm.DB, uid uint, cursor time.Time, limit int) ([]postRow, error) { 92 ctx, span := tracer.Start(ctx, "getTimelineQuery") 93 defer span.End() 94 95 var rows []postRow 96 err := db.Raw(` 97 SELECT 98 'at://' || r.did || '/app.bsky.feed.post/' || p.rkey as uri, 99 p.author as author_id 100 FROM posts p 101 JOIN repos r ON r.id = p.author 102 WHERE p.reply_to = 0 103 AND p.author IN (SELECT subject FROM follows WHERE author = ?) 104 AND p.created < ? 105 AND p.not_found = false 106 ORDER BY p.created DESC 107 LIMIT ? 108 `, uid, cursor, limit).Scan(&rows).Error 109 110 if err != nil { 111 return nil, err 112 } 113 return rows, nil 114}