1package server
2
3import (
4 "strconv"
5
6 "github.com/Azure/go-autorest/autorest/to"
7 "github.com/bluesky-social/indigo/atproto/atdata"
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "github.com/haileyok/cocoon/internal/helpers"
10 "github.com/haileyok/cocoon/models"
11 "github.com/labstack/echo/v4"
12)
13
14type ComAtprotoRepoListRecordsRequest struct {
15 Repo string `query:"repo" validate:"required"`
16 Collection string `query:"collection" validate:"required,atproto-nsid"`
17 Limit int64 `query:"limit"`
18 Cursor string `query:"cursor"`
19 Reverse bool `query:"reverse"`
20}
21
22type ComAtprotoRepoListRecordsResponse struct {
23 Cursor *string `json:"cursor,omitempty"`
24 Records []ComAtprotoRepoListRecordsRecordItem `json:"records"`
25}
26
27type ComAtprotoRepoListRecordsRecordItem struct {
28 Uri string `json:"uri"`
29 Cid string `json:"cid"`
30 Value map[string]any `json:"value"`
31}
32
33func getLimitFromContext(e echo.Context, def int) (int, error) {
34 limit := def
35 limitstr := e.QueryParam("limit")
36
37 if limitstr != "" {
38 l64, err := strconv.ParseInt(limitstr, 10, 32)
39 if err != nil {
40 return 0, err
41 }
42 limit = int(l64)
43 }
44
45 return limit, nil
46}
47
48func (s *Server) handleListRecords(e echo.Context) error {
49 ctx := e.Request().Context()
50 logger := s.logger.With("name", "handleListRecords")
51
52 var req ComAtprotoRepoListRecordsRequest
53 if err := e.Bind(&req); err != nil {
54 logger.Error("could not bind list records request", "error", err)
55 return helpers.ServerError(e, nil)
56 }
57
58 if err := e.Validate(req); err != nil {
59 return helpers.InputError(e, nil)
60 }
61
62 if req.Limit <= 0 {
63 req.Limit = 50
64 } else if req.Limit > 100 {
65 req.Limit = 100
66 }
67
68 limit, err := getLimitFromContext(e, 50)
69 if err != nil {
70 return helpers.InputError(e, nil)
71 }
72
73 sort := "DESC"
74 dir := "<"
75 cursorquery := ""
76
77 if req.Reverse {
78 sort = "ASC"
79 dir = ">"
80 }
81
82 did := req.Repo
83 if _, err := syntax.ParseDID(did); err != nil {
84 actor, err := s.getActorByHandle(ctx, req.Repo)
85 if err != nil {
86 return helpers.InputError(e, to.StringPtr("RepoNotFound"))
87 }
88 did = actor.Did
89 }
90
91 params := []any{did, req.Collection}
92 if req.Cursor != "" {
93 params = append(params, req.Cursor)
94 cursorquery = "AND created_at " + dir + " ?"
95 }
96 params = append(params, limit)
97
98 var records []models.Record
99 if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil {
100 logger.Error("error getting records", "error", err)
101 return helpers.ServerError(e, nil)
102 }
103
104 items := []ComAtprotoRepoListRecordsRecordItem{}
105 for _, r := range records {
106 val, err := atdata.UnmarshalCBOR(r.Value)
107 if err != nil {
108 return err
109 }
110
111 items = append(items, ComAtprotoRepoListRecordsRecordItem{
112 Uri: "at://" + r.Did + "/" + r.Nsid + "/" + r.Rkey,
113 Cid: r.Cid,
114 Value: val,
115 })
116 }
117
118 var newcursor *string
119 if len(records) == limit {
120 newcursor = to.StringPtr(records[len(records)-1].CreatedAt)
121 }
122
123 return e.JSON(200, ComAtprotoRepoListRecordsResponse{
124 Cursor: newcursor,
125 Records: items,
126 })
127}