Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503package db
import ( "database/sql" "fmt" "log" "net/url" "slices" "strings" "time"
"github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/appview/models")
const TimeframeMonths = 7
func MakeProfileTimeline(e Execer, forDid string) (*models.ProfileTimeline, error) { timeline := models.ProfileTimeline{ ByMonth: make([]models.ByMonth, TimeframeMonths), } currentMonth := time.Now().Month() timeframe := fmt.Sprintf("-%d months", TimeframeMonths)
pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) if err != nil { return nil, fmt.Errorf("error getting pulls by owner did: %w", err) }
// group pulls by month for _, pull := range pulls { pullMonth := pull.Created.Month()
if currentMonth-pullMonth >= TimeframeMonths { // shouldn't happen; but times are weird continue }
idx := currentMonth - pullMonth items := &timeline.ByMonth[idx].PullEvents.Items
*items = append(*items, &pull) }
issues, err := GetIssues( e, FilterEq("did", forDid), FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)), ) if err != nil { return nil, fmt.Errorf("error getting issues by owner did: %w", err) }
for _, issue := range issues { issueMonth := issue.Created.Month()
if currentMonth-issueMonth >= TimeframeMonths { // shouldn't happen; but times are weird continue }
idx := currentMonth - issueMonth items := &timeline.ByMonth[idx].IssueEvents.Items
*items = append(*items, &issue) }
repos, err := GetRepos(e, 0, FilterEq("did", forDid)) if err != nil { return nil, fmt.Errorf("error getting all repos by did: %w", err) }
for _, repo := range repos { // TODO: get this in the original query; requires COALESCE because nullable var sourceRepo *models.Repo if repo.Source != "" { sourceRepo, err = GetRepoByAtUri(e, repo.Source) if err != nil { return nil, err } }
repoMonth := repo.Created.Month()
if currentMonth-repoMonth >= TimeframeMonths { // shouldn't happen; but times are weird continue }
idx := currentMonth - repoMonth
items := &timeline.ByMonth[idx].RepoEvents *items = append(*items, models.RepoEvent{ Repo: &repo, Source: sourceRepo, }) }
return &timeline, nil}
func UpsertProfile(tx *sql.Tx, profile *models.Profile) error { defer tx.Rollback()
// update links _, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did) if err != nil { return err } // update vanity stats _, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did) if err != nil { return err }
// update pinned repos _, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did) if err != nil { return err }
includeBskyValue := 0 if profile.IncludeBluesky { includeBskyValue = 1 }
_, err = tx.Exec( `insert or replace into profile ( did, description, include_bluesky, location, pronouns ) values (?, ?, ?, ?, ?)`, profile.Did, profile.Description, includeBskyValue, profile.Location, profile.Pronouns, )
if err != nil { log.Println("profile", "err", err) return err }
for _, link := range profile.Links { if link == "" { continue }
_, err := tx.Exec( `insert into profile_links (did, link) values (?, ?)`, profile.Did, link, )
if err != nil { log.Println("profile_links", "err", err) return err } }
for _, v := range profile.Stats { if v.Kind == "" { continue }
_, err := tx.Exec( `insert into profile_stats (did, kind) values (?, ?)`, profile.Did, v.Kind, )
if err != nil { log.Println("profile_stats", "err", err) return err } }
for _, pin := range profile.PinnedRepos { if pin == "" { continue }
_, err := tx.Exec( `insert into profile_pinned_repositories (did, at_uri) values (?, ?)`, profile.Did, pin, )
if err != nil { log.Println("profile_pinned_repositories", "err", err) return err } }
return tx.Commit()}
func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, 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 ") }
profilesQuery := fmt.Sprintf( `select id, did, description, include_bluesky, location, pronouns from profile %s`, whereClause, ) rows, err := e.Query(profilesQuery, args...) if err != nil { return nil, err }
profileMap := make(map[string]*models.Profile) for rows.Next() { var profile models.Profile var includeBluesky int var pronouns sql.Null[string]
err = rows.Scan(&profile.ID, &profile.Did, &profile.Description, &includeBluesky, &profile.Location, &pronouns) if err != nil { return nil, err }
if includeBluesky != 0 { profile.IncludeBluesky = true }
if pronouns.Valid { profile.Pronouns = pronouns.V }
profileMap[profile.Did] = &profile } if err = rows.Err(); err != nil { return nil, err }
// populate profile links inClause := strings.TrimSuffix(strings.Repeat("?, ", len(profileMap)), ", ") args = make([]any, len(profileMap)) i := 0 for did := range profileMap { args[i] = did i++ }
linksQuery := fmt.Sprintf("select link, did from profile_links where did in (%s)", inClause) rows, err = e.Query(linksQuery, args...) if err != nil { return nil, err } idxs := make(map[string]int) for did := range profileMap { idxs[did] = 0 } for rows.Next() { var link, did string if err = rows.Scan(&link, &did); err != nil { return nil, err }
idx := idxs[did] profileMap[did].Links[idx] = link idxs[did] = idx + 1 }
pinsQuery := fmt.Sprintf("select at_uri, did from profile_pinned_repositories where did in (%s)", inClause) rows, err = e.Query(pinsQuery, args...) if err != nil { return nil, err } idxs = make(map[string]int) for did := range profileMap { idxs[did] = 0 } for rows.Next() { var link syntax.ATURI var did string if err = rows.Scan(&link, &did); err != nil { return nil, err }
idx := idxs[did] profileMap[did].PinnedRepos[idx] = link idxs[did] = idx + 1 }
return profileMap, nil}
func GetProfile(e Execer, did string) (*models.Profile, error) { var profile models.Profile var pronouns sql.Null[string]
profile.Did = did
includeBluesky := 0
err := e.QueryRow( `select description, include_bluesky, location, pronouns from profile where did = ?`, did, ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns) if err == sql.ErrNoRows { profile := models.Profile{} profile.Did = did return &profile, nil }
if err != nil { return nil, err }
if includeBluesky != 0 { profile.IncludeBluesky = true }
if pronouns.Valid { profile.Pronouns = pronouns.V }
rows, err := e.Query(`select link from profile_links where did = ?`, did) if err != nil { return nil, err } defer rows.Close() i := 0 for rows.Next() { if err := rows.Scan(&profile.Links[i]); err != nil { return nil, err } i++ }
rows, err = e.Query(`select kind from profile_stats where did = ?`, did) if err != nil { return nil, err } defer rows.Close() i = 0 for rows.Next() { if err := rows.Scan(&profile.Stats[i].Kind); err != nil { return nil, err } value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind) if err != nil { return nil, err } profile.Stats[i].Value = value i++ }
rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did) if err != nil { return nil, err } defer rows.Close() i = 0 for rows.Next() { if err := rows.Scan(&profile.PinnedRepos[i]); err != nil { return nil, err } i++ }
return &profile, nil}
func GetVanityStat(e Execer, did string, stat models.VanityStatKind) (uint64, error) { query := "" var args []any switch stat { case models.VanityStatMergedPRCount: query = `select count(id) from pulls where owner_did = ? and state = ?` args = append(args, did, models.PullMerged) case models.VanityStatClosedPRCount: query = `select count(id) from pulls where owner_did = ? and state = ?` args = append(args, did, models.PullClosed) case models.VanityStatOpenPRCount: query = `select count(id) from pulls where owner_did = ? and state = ?` args = append(args, did, models.PullOpen) case models.VanityStatOpenIssueCount: query = `select count(id) from issues where did = ? and open = 1` args = append(args, did) case models.VanityStatClosedIssueCount: query = `select count(id) from issues where did = ? and open = 0` args = append(args, did) case models.VanityStatRepositoryCount: query = `select count(id) from repos where did = ?` args = append(args, did) }
var result uint64 err := e.QueryRow(query, args...).Scan(&result) if err != nil { return 0, err }
return result, nil}
func ValidateProfile(e Execer, profile *models.Profile) error { // ensure description is not too long if len(profile.Description) > 256 { return fmt.Errorf("Entered bio is too long.") }
// ensure description is not too long if len(profile.Location) > 40 { return fmt.Errorf("Entered location is too long.") }
// ensure pronouns are not too long if len(profile.Pronouns) > 40 { return fmt.Errorf("Entered pronouns are too long.") }
// ensure links are in order err := validateLinks(profile) if err != nil { return err }
// ensure all pinned repos are either own repos or collaborating repos repos, err := GetRepos(e, 0, FilterEq("did", profile.Did)) if err != nil { log.Printf("getting repos for %s: %s", profile.Did, err) }
collaboratingRepos, err := CollaboratingIn(e, profile.Did) if err != nil { log.Printf("getting collaborating repos for %s: %s", profile.Did, err) }
var validRepos []syntax.ATURI for _, r := range repos { validRepos = append(validRepos, r.RepoAt()) } for _, r := range collaboratingRepos { validRepos = append(validRepos, r.RepoAt()) }
for _, pinned := range profile.PinnedRepos { if pinned == "" { continue } if !slices.Contains(validRepos, pinned) { return fmt.Errorf("Invalid pinned repo: `%s, does not belong to own or collaborating repos", pinned) } }
return nil}
func validateLinks(profile *models.Profile) error { for i, link := range profile.Links { if link == "" { continue }
parsedURL, err := url.Parse(link) if err != nil { return fmt.Errorf("Invalid URL '%s': %v\n", link, err) }
if parsedURL.Scheme == "" { if strings.HasPrefix(link, "//") { profile.Links[i] = "https:" + link } else { profile.Links[i] = "https://" + link } continue } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { return fmt.Errorf("Warning: URL '%s' has unusual scheme: %s\n", link, parsedURL.Scheme) }
// catch relative paths if parsedURL.Host == "" { return fmt.Errorf("Warning: URL '%s' appears to be a relative path\n", link) } } return nil}