1package db
2
3import (
4 "fmt"
5 "log"
6 "strings"
7 "time"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10)
11
12type Star struct {
13 StarredByDid string
14 RepoAt syntax.ATURI
15 Created time.Time
16 Rkey string
17
18 // optionally, populate this when querying for reverse mappings
19 Repo *Repo
20}
21
22func (star *Star) ResolveRepo(e Execer) error {
23 if star.Repo != nil {
24 return nil
25 }
26
27 repo, err := GetRepoByAtUri(e, star.RepoAt.String())
28 if err != nil {
29 return err
30 }
31
32 star.Repo = repo
33 return nil
34}
35
36func AddStar(e Execer, star *Star) error {
37 query := `insert or ignore into stars (starred_by_did, repo_at, rkey) values (?, ?, ?)`
38 _, err := e.Exec(
39 query,
40 star.StarredByDid,
41 star.RepoAt.String(),
42 star.Rkey,
43 )
44 return err
45}
46
47// Get a star record
48func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) {
49 query := `
50 select starred_by_did, repo_at, created, rkey
51 from stars
52 where starred_by_did = ? and repo_at = ?`
53 row := e.QueryRow(query, starredByDid, repoAt)
54
55 var star Star
56 var created string
57 err := row.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
58 if err != nil {
59 return nil, err
60 }
61
62 createdAtTime, err := time.Parse(time.RFC3339, created)
63 if err != nil {
64 log.Println("unable to determine followed at time")
65 star.Created = time.Now()
66 } else {
67 star.Created = createdAtTime
68 }
69
70 return &star, nil
71}
72
73// Remove a star
74func DeleteStar(e Execer, starredByDid string, repoAt syntax.ATURI) error {
75 _, err := e.Exec(`delete from stars where starred_by_did = ? and repo_at = ?`, starredByDid, repoAt)
76 return err
77}
78
79// Remove a star
80func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error {
81 _, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey)
82 return err
83}
84
85func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) {
86 stars := 0
87 err := e.QueryRow(
88 `select count(starred_by_did) from stars where repo_at = ?`, repoAt).Scan(&stars)
89 if err != nil {
90 return 0, err
91 }
92 return stars, nil
93}
94
95func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool {
96 if _, err := GetStar(e, userDid, repoAt); err != nil {
97 return false
98 } else {
99 return true
100 }
101}
102
103func GetStars(e Execer, limit int, filters ...filter) ([]Star, error) {
104 var conditions []string
105 var args []any
106 for _, filter := range filters {
107 conditions = append(conditions, filter.Condition())
108 args = append(args, filter.Arg()...)
109 }
110
111 whereClause := ""
112 if conditions != nil {
113 whereClause = " where " + strings.Join(conditions, " and ")
114 }
115
116 limitClause := ""
117 if limit != 0 {
118 limitClause = fmt.Sprintf(" limit %d", limit)
119 }
120
121 repoQuery := fmt.Sprintf(
122 `select starred_by_did, repo_at, created, rkey
123 from stars
124 %s
125 order by created desc
126 %s`,
127 whereClause,
128 limitClause,
129 )
130 rows, err := e.Query(repoQuery, args...)
131 if err != nil {
132 return nil, err
133 }
134
135 starMap := make(map[string][]Star)
136 for rows.Next() {
137 var star Star
138 var created string
139 err := rows.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
140 if err != nil {
141 return nil, err
142 }
143
144 star.Created = time.Now()
145 if t, err := time.Parse(time.RFC3339, created); err == nil {
146 star.Created = t
147 }
148
149 repoAt := string(star.RepoAt)
150 starMap[repoAt] = append(starMap[repoAt], star)
151 }
152
153 // populate *Repo in each star
154 args = make([]any, len(starMap))
155 i := 0
156 for r := range starMap {
157 args[i] = r
158 i++
159 }
160
161 if len(args) == 0 {
162 return nil, nil
163 }
164
165 repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
166 if err != nil {
167 return nil, err
168 }
169
170 for _, r := range repos {
171 if stars, ok := starMap[string(r.RepoAt())]; ok {
172 for i := range stars {
173 stars[i].Repo = &r
174 }
175 }
176 }
177
178 var stars []Star
179 for _, s := range starMap {
180 stars = append(stars, s...)
181 }
182
183 return stars, nil
184}
185
186func GetAllStars(e Execer, limit int) ([]Star, error) {
187 var stars []Star
188
189 rows, err := e.Query(`
190 select
191 s.starred_by_did,
192 s.repo_at,
193 s.rkey,
194 s.created,
195 r.did,
196 r.name,
197 r.knot,
198 r.rkey,
199 r.created,
200 r.at_uri
201 from stars s
202 join repos r on s.repo_at = r.at_uri
203 `)
204
205 if err != nil {
206 return nil, err
207 }
208 defer rows.Close()
209
210 for rows.Next() {
211 var star Star
212 var repo Repo
213 var starCreatedAt, repoCreatedAt string
214
215 if err := rows.Scan(
216 &star.StarredByDid,
217 &star.RepoAt,
218 &star.Rkey,
219 &starCreatedAt,
220 &repo.Did,
221 &repo.Name,
222 &repo.Knot,
223 &repo.Rkey,
224 &repoCreatedAt,
225 &repo.AtUri,
226 ); err != nil {
227 return nil, err
228 }
229
230 star.Created, err = time.Parse(time.RFC3339, starCreatedAt)
231 if err != nil {
232 star.Created = time.Now()
233 }
234 repo.Created, err = time.Parse(time.RFC3339, repoCreatedAt)
235 if err != nil {
236 repo.Created = time.Now()
237 }
238 star.Repo = &repo
239
240 stars = append(stars, star)
241 }
242
243 if err := rows.Err(); err != nil {
244 return nil, err
245 }
246
247 return stars, nil
248}