···1+package main
2+3+import (
4+ "context"
5+ "fmt"
6+ "log/slog"
7+ "strconv"
8+)
9+10+// Post describes a Bluesky post
11+type Post struct {
12+ ID int
13+ RKey string
14+ PostURI string
15+ UserDID string
16+ CreatedAt int64
17+}
18+19+// PostStore defines the interactions with a store
20+type PostStore interface {
21+ GetFeedPosts(cursor, limit int) ([]Post, error)
22+ CreatePost(post Post) error
23+}
24+25+// FeedGenerator is responsible for generating a feed
26+type FeedGenerator struct {
27+ store PostStore
28+}
29+30+func newFeedGenerator(store PostStore) *FeedGenerator {
31+ return &FeedGenerator{
32+ store: store,
33+ }
34+}
35+36+// GetFeed will fetch a feed and build up a response that can be returned
37+func (f *FeedGenerator) GetFeed(ctx context.Context, feed, cursor string, limit int) (FeedSkeletonReponse, error) {
38+ resp := FeedSkeletonReponse{
39+ Feed: make([]FeedSkeletonPost, 0),
40+ }
41+42+ cursorInt, err := strconv.Atoi(cursor)
43+ if err != nil && cursor != "" {
44+ slog.Error("convert cursor to int", "error", err, "cursor value", cursor)
45+ }
46+ if cursorInt == 0 {
47+ // if no cursor provided use a date waaaaay in the future to start the less than query
48+ cursorInt = 9999999999999
49+ }
50+51+ posts, err := f.store.GetFeedPosts(cursorInt, limit)
52+ if err != nil {
53+ return resp, fmt.Errorf("get feed from DB: %w", err)
54+ }
55+56+ usersFeed := make([]FeedSkeletonPost, 0, len(posts))
57+ for _, post := range posts {
58+ usersFeed = append(usersFeed, FeedSkeletonPost{
59+ Post: post.PostURI,
60+ })
61+ }
62+63+ resp.Feed = usersFeed
64+65+ // only set the return cursor if there was at least 1 record returned and that the len of records
66+ // being returned is the same as the limit
67+ if len(posts) > 0 && len(posts) == limit {
68+ lastPost := posts[len(posts)-1]
69+ resp.Cursor = fmt.Sprintf("%d", lastPost.CreatedAt)
70+ }
71+ return resp, nil
72+}