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