package db import ( "fmt" "log" "slices" "strings" "time" "tangled.org/core/appview/models" "tangled.org/core/appview/pagination" "tangled.org/core/orm" ) func AddStar(e Execer, star *models.Star) error { query := `insert or ignore into stars (did, subject_type, subject, rkey) values (?, ?, ?, ?)` _, err := e.Exec( query, star.Did, string(star.SubjectType), star.Subject, star.Rkey, ) return err } // Get a star record func GetStar(e Execer, did string, subject string) (*models.Star, error) { query := ` select did, subject_type, subject, created, rkey from stars where did = ? and subject = ?` row := e.QueryRow(query, did, subject) var star models.Star var created string err := row.Scan(&star.Did, &star.SubjectType, &star.Subject, &created, &star.Rkey) if err != nil { return nil, err } createdAtTime, err := time.Parse(time.RFC3339, created) if err != nil { log.Println("unable to determine followed at time") star.Created = time.Now() } else { star.Created = createdAtTime } return &star, nil } func GetStars(e Execer, subject string, page pagination.Page) ([]models.Star, error) { query := ` select did, subject_type, subject, created, rkey from stars where subject = ? order by created desc limit ? offset ? ` rows, err := e.Query(query, subject, page.Limit, page.Offset) if err != nil { return nil, err } defer rows.Close() var stars []models.Star for rows.Next() { var star models.Star var created string if err := rows.Scan(&star.Did, &star.SubjectType, &star.Subject, &created, &star.Rkey); err != nil { return nil, err } star.Created = time.Now() if t, err := time.Parse(time.RFC3339, created); err == nil { star.Created = t } stars = append(stars, star) } return stars, rows.Err() } // Remove a star func DeleteStar(e Execer, did string, subject string) error { _, err := e.Exec(`delete from stars where did = ? and subject = ?`, did, subject) return err } // Remove a star func DeleteStarByRkey(e Execer, did string, rkey string) error { _, err := e.Exec(`delete from stars where did = ? and rkey = ?`, did, rkey) return err } func GetStarCount(e Execer, subjectType models.StarSubjectType, subject string) (int, error) { stars := 0 err := e.QueryRow( `select count(did) from stars where subject_type = ? and subject = ?`, string(subjectType), subject, ).Scan(&stars) if err != nil { return 0, err } return stars, nil } // getStarStatuses returns a map of subjects to star status for a given user // This is an internal helper function to avoid N+1 queries func getStarStatuses(e Execer, userDid string, subjects []string) (map[string]bool, error) { if len(subjects) == 0 || userDid == "" { return make(map[string]bool), nil } placeholders := make([]string, len(subjects)) args := make([]any, len(subjects)+1) args[0] = userDid for i, subj := range subjects { placeholders[i] = "?" args[i+1] = subj } query := fmt.Sprintf(` SELECT subject FROM stars WHERE did = ? AND subject IN (%s) `, strings.Join(placeholders, ",")) rows, err := e.Query(query, args...) if err != nil { return nil, err } defer rows.Close() result := make(map[string]bool) // Initialize all subjects as not starred for _, subj := range subjects { result[subj] = false } // Mark starred subjects as true for rows.Next() { var subj string if err := rows.Scan(&subj); err != nil { return nil, err } result[subj] = true } return result, nil } func GetStarStatus(e Execer, userDid string, subject string) bool { statuses, err := getStarStatuses(e, userDid, []string{subject}) if err != nil { return false } return statuses[subject] } // GetStarStatuses returns a map of subjects to star status for a given user func GetStarStatuses(e Execer, userDid string, subjects []string) (map[string]bool, error) { return getStarStatuses(e, userDid, subjects) } // GetRepoStars return a list of stars each holding target repository. // If there isn't known repo with starred at-uri, those stars will be ignored. func GetRepoStars(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.RepoStar, error) { var conditions []string var args []any for _, filter := range filters { conditions = append(conditions, filter.Condition()) args = append(args, filter.Arg()...) } conditions = append(conditions, "subject_type = 'repo'") whereClause := " where " + strings.Join(conditions, " and ") pageClause := "" if page.Limit != 0 { pageClause = fmt.Sprintf(" limit %d offset %d", page.Limit, page.Offset) } repoQuery := fmt.Sprintf( `select did, subject_type, subject, created, rkey from stars %s order by created desc %s`, whereClause, pageClause, ) rows, err := e.Query(repoQuery, args...) if err != nil { return nil, err } defer rows.Close() starMap := make(map[string][]models.Star) for rows.Next() { var star models.Star var created string err := rows.Scan(&star.Did, &star.SubjectType, &star.Subject, &created, &star.Rkey) if err != nil { return nil, err } star.Created = time.Now() if t, err := time.Parse(time.RFC3339, created); err == nil { star.Created = t } starMap[star.Subject] = append(starMap[star.Subject], star) } // populate *Repo in each star args = make([]any, len(starMap)) i := 0 for r := range starMap { args[i] = r i++ } if len(args) == 0 { return nil, nil } repos, err := GetRepos(e, orm.FilterIn("repo_did", args)) if err != nil { return nil, err } var repoStars []models.RepoStar for _, r := range repos { if stars, ok := starMap[r.RepoDid]; ok { for _, star := range stars { repoStars = append(repoStars, models.RepoStar{ Star: star, Repo: &r, }) } } } slices.SortFunc(repoStars, func(a, b models.RepoStar) int { if a.Created.After(b.Created) { return -1 } if b.Created.After(a.Created) { return 1 } return 0 }) return repoStars, nil } func CountStars(e Execer, filters ...orm.Filter) (int64, error) { var conditions []string var args []any for _, filter := range filters { conditions = append(conditions, filter.Condition()) args = append(args, filter.Arg()...) } whereClause := "" if conditions != nil { whereClause = " where " + strings.Join(conditions, " and ") } repoQuery := fmt.Sprintf(`select count(1) from stars %s`, whereClause) var count int64 if err := e.QueryRow(repoQuery, args...).Scan(&count); err != nil { return 0, err } return count, nil } // GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week func GetTopStarredReposLastWeek(e Execer) ([]models.Repo, error) { // first, get the top repo DIDs by star count from the last week query := ` with recent_starred_repos as ( select distinct subject from stars where created >= datetime('now', '-7 days') and subject_type = 'repo' ), repo_star_counts as ( select s.subject, count(*) as stars_gained_last_week from stars s join recent_starred_repos rsr on s.subject = rsr.subject where s.created >= datetime('now', '-7 days') and s.subject_type = 'repo' group by s.subject ) select rsc.subject from repo_star_counts rsc order by rsc.stars_gained_last_week desc limit 5 ` rows, err := e.Query(query) if err != nil { return nil, err } defer rows.Close() var repoDids []string for rows.Next() { var repoDid string err := rows.Scan(&repoDid) if err != nil { return nil, err } repoDids = append(repoDids, repoDid) } if err := rows.Err(); err != nil { return nil, err } if len(repoDids) == 0 { return []models.Repo{}, nil } // get full repo data repos, err := GetRepos(e, orm.FilterIn("repo_did", repoDids)) if err != nil { return nil, err } // sort repos by the original trending order repoMap := make(map[string]models.Repo) for _, repo := range repos { repoMap[repo.RepoDid] = repo } orderedRepos := make([]models.Repo, 0, len(repoDids)) for _, did := range repoDids { if repo, exists := repoMap[did]; exists { orderedRepos = append(orderedRepos, repo) } } return orderedRepos, nil }