forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package db
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "log"
8 "slices"
9 "strings"
10 "time"
11
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 "tangled.org/core/appview/models"
14 "tangled.org/core/orm"
15)
16
17func AddStar(e Execer, star *models.Star) error {
18 query := `insert or ignore into stars (did, subject_at, rkey) values (?, ?, ?)`
19 _, err := e.Exec(
20 query,
21 star.Did,
22 star.RepoAt.String(),
23 star.Rkey,
24 )
25 return err
26}
27
28// Get a star record
29func GetStar(e Execer, did string, subjectAt syntax.ATURI) (*models.Star, error) {
30 query := `
31 select did, subject_at, created, rkey
32 from stars
33 where did = ? and subject_at = ?`
34 row := e.QueryRow(query, did, subjectAt)
35
36 var star models.Star
37 var created string
38 err := row.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey)
39 if err != nil {
40 return nil, err
41 }
42
43 createdAtTime, err := time.Parse(time.RFC3339, created)
44 if err != nil {
45 log.Println("unable to determine followed at time")
46 star.Created = time.Now()
47 } else {
48 star.Created = createdAtTime
49 }
50
51 return &star, nil
52}
53
54// Remove a star
55func DeleteStar(e Execer, did string, subjectAt syntax.ATURI) error {
56 _, err := e.Exec(`delete from stars where did = ? and subject_at = ?`, did, subjectAt)
57 return err
58}
59
60// Remove a star
61func DeleteStarByRkey(e Execer, did string, rkey string) error {
62 _, err := e.Exec(`delete from stars where did = ? and rkey = ?`, did, rkey)
63 return err
64}
65
66func GetStarCount(e Execer, subjectAt syntax.ATURI) (int, error) {
67 stars := 0
68 err := e.QueryRow(
69 `select count(did) from stars where subject_at = ?`, subjectAt).Scan(&stars)
70 if err != nil {
71 return 0, err
72 }
73 return stars, nil
74}
75
76// getStarStatuses returns a map of repo URIs to star status for a given user
77// This is an internal helper function to avoid N+1 queries
78func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) {
79 if len(repoAts) == 0 || userDid == "" {
80 return make(map[string]bool), nil
81 }
82
83 placeholders := make([]string, len(repoAts))
84 args := make([]any, len(repoAts)+1)
85 args[0] = userDid
86
87 for i, repoAt := range repoAts {
88 placeholders[i] = "?"
89 args[i+1] = repoAt.String()
90 }
91
92 query := fmt.Sprintf(`
93 SELECT subject_at
94 FROM stars
95 WHERE did = ? AND subject_at IN (%s)
96 `, strings.Join(placeholders, ","))
97
98 rows, err := e.Query(query, args...)
99 if err != nil {
100 return nil, err
101 }
102 defer rows.Close()
103
104 result := make(map[string]bool)
105 // Initialize all repos as not starred
106 for _, repoAt := range repoAts {
107 result[repoAt.String()] = false
108 }
109
110 // Mark starred repos as true
111 for rows.Next() {
112 var repoAt string
113 if err := rows.Scan(&repoAt); err != nil {
114 return nil, err
115 }
116 result[repoAt] = true
117 }
118
119 return result, nil
120}
121
122func GetStarStatus(e Execer, userDid string, subjectAt syntax.ATURI) bool {
123 statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{subjectAt})
124 if err != nil {
125 return false
126 }
127 return statuses[subjectAt.String()]
128}
129
130// GetStarStatuses returns a map of repo URIs to star status for a given user
131func GetStarStatuses(e Execer, userDid string, subjectAts []syntax.ATURI) (map[string]bool, error) {
132 return getStarStatuses(e, userDid, subjectAts)
133}
134
135// GetRepoStars return a list of stars each holding target repository.
136// If there isn't known repo with starred at-uri, those stars will be ignored.
137func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) {
138 var conditions []string
139 var args []any
140 for _, filter := range filters {
141 conditions = append(conditions, filter.Condition())
142 args = append(args, filter.Arg()...)
143 }
144
145 whereClause := ""
146 if conditions != nil {
147 whereClause = " where " + strings.Join(conditions, " and ")
148 }
149
150 limitClause := ""
151 if limit != 0 {
152 limitClause = fmt.Sprintf(" limit %d", limit)
153 }
154
155 repoQuery := fmt.Sprintf(
156 `select did, subject_at, created, rkey
157 from stars
158 %s
159 order by created desc
160 %s`,
161 whereClause,
162 limitClause,
163 )
164 rows, err := e.Query(repoQuery, args...)
165 if err != nil {
166 return nil, err
167 }
168 defer rows.Close()
169
170 starMap := make(map[string][]models.Star)
171 for rows.Next() {
172 var star models.Star
173 var created string
174 err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey)
175 if err != nil {
176 return nil, err
177 }
178
179 star.Created = time.Now()
180 if t, err := time.Parse(time.RFC3339, created); err == nil {
181 star.Created = t
182 }
183
184 repoAt := string(star.RepoAt)
185 starMap[repoAt] = append(starMap[repoAt], star)
186 }
187
188 // populate *Repo in each star
189 args = make([]any, len(starMap))
190 i := 0
191 for r := range starMap {
192 args[i] = r
193 i++
194 }
195
196 if len(args) == 0 {
197 return nil, nil
198 }
199
200 repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args))
201 if err != nil {
202 return nil, err
203 }
204
205 var repoStars []models.RepoStar
206 for _, r := range repos {
207 if stars, ok := starMap[string(r.RepoAt())]; ok {
208 for _, star := range stars {
209 repoStars = append(repoStars, models.RepoStar{
210 Star: star,
211 Repo: &r,
212 })
213 }
214 }
215 }
216
217 slices.SortFunc(repoStars, func(a, b models.RepoStar) int {
218 if a.Created.After(b.Created) {
219 return -1
220 }
221 if b.Created.After(a.Created) {
222 return 1
223 }
224 return 0
225 })
226
227 return repoStars, nil
228}
229
230func CountStars(e Execer, filters ...orm.Filter) (int64, error) {
231 var conditions []string
232 var args []any
233 for _, filter := range filters {
234 conditions = append(conditions, filter.Condition())
235 args = append(args, filter.Arg()...)
236 }
237
238 whereClause := ""
239 if conditions != nil {
240 whereClause = " where " + strings.Join(conditions, " and ")
241 }
242
243 repoQuery := fmt.Sprintf(`select count(1) from stars %s`, whereClause)
244 var count int64
245 err := e.QueryRow(repoQuery, args...).Scan(&count)
246
247 if !errors.Is(err, sql.ErrNoRows) && err != nil {
248 return 0, err
249 }
250
251 return count, nil
252}
253
254// GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week
255func GetTopStarredReposLastWeek(e Execer) ([]models.Repo, error) {
256 // first, get the top repo URIs by star count from the last week
257 query := `
258 with recent_starred_repos as (
259 select distinct subject_at
260 from stars
261 where created >= datetime('now', '-7 days')
262 ),
263 repo_star_counts as (
264 select
265 s.subject_at,
266 count(*) as stars_gained_last_week
267 from stars s
268 join recent_starred_repos rsr on s.subject_at = rsr.subject_at
269 where s.created >= datetime('now', '-7 days')
270 group by s.subject_at
271 )
272 select rsc.subject_at
273 from repo_star_counts rsc
274 order by rsc.stars_gained_last_week desc
275 limit 8
276 `
277
278 rows, err := e.Query(query)
279 if err != nil {
280 return nil, err
281 }
282 defer rows.Close()
283
284 var repoUris []string
285 for rows.Next() {
286 var repoUri string
287 err := rows.Scan(&repoUri)
288 if err != nil {
289 return nil, err
290 }
291 repoUris = append(repoUris, repoUri)
292 }
293
294 if err := rows.Err(); err != nil {
295 return nil, err
296 }
297
298 if len(repoUris) == 0 {
299 return []models.Repo{}, nil
300 }
301
302 // get full repo data
303 repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris))
304 if err != nil {
305 return nil, err
306 }
307
308 // sort repos by the original trending order
309 repoMap := make(map[string]models.Repo)
310 for _, repo := range repos {
311 repoMap[repo.RepoAt().String()] = repo
312 }
313
314 orderedRepos := make([]models.Repo, 0, len(repoUris))
315 for _, uri := range repoUris {
316 if repo, exists := repoMap[uri]; exists {
317 orderedRepos = append(orderedRepos, repo)
318 }
319 }
320
321 return orderedRepos, nil
322}