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
51 var req ComAtprotoRepoListRecordsRequest
52 if err := e.Bind(&req); err != nil {
53 s.logger.Error("could not bind list records request", "error", err)
54 return helpers.ServerError(e, nil)
55 }
56
57 if err := e.Validate(req); err != nil {
58 return helpers.InputError(e, nil)
59 }
60
61 if req.Limit <= 0 {
62 req.Limit = 50
63 } else if req.Limit > 100 {
64 req.Limit = 100
65 }
66
67 limit, err := getLimitFromContext(e, 50)
68 if err != nil {
69 return helpers.InputError(e, nil)
70 }
71
72 sort := "DESC"
73 dir := "<"
74 cursorquery := ""
75
76 if req.Reverse {
77 sort = "ASC"
78 dir = ">"
79 }
80
81 did := req.Repo
82 if _, err := syntax.ParseDID(did); err != nil {
83 actor, err := s.getActorByHandle(ctx, req.Repo)
84 if err != nil {
85 return helpers.InputError(e, to.StringPtr("RepoNotFound"))
86 }
87 did = actor.Did
88 }
89
90 params := []any{did, req.Collection}
91 if req.Cursor != "" {
92 params = append(params, req.Cursor)
93 cursorquery = "AND created_at " + dir + " ?"
94 }
95 params = append(params, limit)
96
97 var records []models.Record
98 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 {
99 s.logger.Error("error getting records", "error", err)
100 return helpers.ServerError(e, nil)
101 }
102
103 items := []ComAtprotoRepoListRecordsRecordItem{}
104 for _, r := range records {
105 val, err := atdata.UnmarshalCBOR(r.Value)
106 if err != nil {
107 return err
108 }
109
110 items = append(items, ComAtprotoRepoListRecordsRecordItem{
111 Uri: "at://" + r.Did + "/" + r.Nsid + "/" + r.Rkey,
112 Cid: r.Cid,
113 Value: val,
114 })
115 }
116
117 var newcursor *string
118 if len(records) == limit {
119 newcursor = to.StringPtr(records[len(records)-1].CreatedAt)
120 }
121
122 return e.JSON(200, ComAtprotoRepoListRecordsResponse{
123 Cursor: newcursor,
124 Records: items,
125 })
126}