project.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: Apache-2.0
  4
  5package project
  6
  7import (
  8	"crypto/sha256"
  9	"database/sql"
 10	"fmt"
 11	"log"
 12	"sort"
 13	"strings"
 14	"time"
 15
 16	"github.com/unascribed/FlexVer/go/flexver"
 17
 18	"git.sr.ht/~amolith/willow/db"
 19	"git.sr.ht/~amolith/willow/git"
 20	"git.sr.ht/~amolith/willow/rss"
 21)
 22
 23type Project struct {
 24	URL      string
 25	Name     string
 26	Forge    string
 27	Running  string
 28	Releases []Release
 29}
 30
 31type Release struct {
 32	URL     string
 33	Tag     string
 34	Content string
 35	Date    time.Time
 36}
 37
 38// GetReleases returns a list of all releases for a project from the database
 39func GetReleases(dbConn *sql.DB, proj Project) (Project, error) {
 40	ret, err := db.GetReleases(dbConn, proj.URL)
 41	if err != nil {
 42		return proj, err
 43	}
 44
 45	if len(ret) == 0 {
 46		return fetchReleases(dbConn, proj)
 47	}
 48
 49	for _, row := range ret {
 50		proj.Releases = append(proj.Releases, Release{
 51			Tag:     row["tag"],
 52			Content: row["content"],
 53			URL:     row["release_url"],
 54			Date:    time.Time{},
 55		})
 56	}
 57	proj.Releases = SortReleases(proj.Releases)
 58	return proj, nil
 59}
 60
 61// fetchReleases fetches releases from a project's forge given its URI
 62func fetchReleases(dbConn *sql.DB, p Project) (Project, error) {
 63	var err error
 64	switch p.Forge {
 65	case "github", "gitea", "forgejo":
 66		rssReleases, err := rss.GetReleases(p.URL)
 67		if err != nil {
 68			fmt.Println("Error getting RSS releases:", err)
 69			return p, err
 70		}
 71		for _, release := range rssReleases {
 72			p.Releases = append(p.Releases, Release{
 73				Tag:     release.Tag,
 74				Content: release.Content,
 75				URL:     release.URL,
 76				Date:    release.Date,
 77			})
 78			err = upsert(dbConn, p.URL, p.Releases)
 79			if err != nil {
 80				log.Printf("Error upserting release: %v", err)
 81				return p, err
 82			}
 83		}
 84	default:
 85		gitReleases, err := git.GetReleases(p.URL, p.Forge)
 86		if err != nil {
 87			return p, err
 88		}
 89		for _, release := range gitReleases {
 90			p.Releases = append(p.Releases, Release{
 91				Tag:     release.Tag,
 92				Content: release.Content,
 93				URL:     release.URL,
 94				Date:    release.Date,
 95			})
 96			err = upsert(dbConn, p.URL, p.Releases)
 97			if err != nil {
 98				log.Printf("Error upserting release: %v", err)
 99				return p, err
100			}
101		}
102	}
103	p.Releases = SortReleases(p.Releases)
104	return p, err
105}
106
107func SortReleases(releases []Release) []Release {
108	sort.Slice(releases, func(i, j int) bool {
109		return !flexver.Less(releases[i].Tag, releases[j].Tag)
110	})
111	return releases
112}
113
114// upsert updates or inserts a project release into the database
115func upsert(dbConn *sql.DB, url string, releases []Release) error {
116	for _, release := range releases {
117		date := release.Date.Format("2006-01-02 15:04:05")
118		id := genReleaseID(url, release.URL, release.Tag)
119		err := db.UpsertRelease(dbConn, id, url, release.URL, release.Tag, release.Content, date)
120		if err != nil {
121			log.Printf("Error upserting release: %v", err)
122			return err
123		}
124	}
125	return nil
126}
127
128func genReleaseID(projectURL, releaseURL, tag string) string {
129	idByte := sha256.Sum256([]byte(projectURL + releaseURL + tag))
130	return fmt.Sprintf("%x", idByte)
131}
132
133func Track(dbConn *sql.DB, manualRefresh *chan struct{}, name, url, forge, release string) {
134	err := db.UpsertProject(dbConn, url, name, forge, release)
135	if err != nil {
136		fmt.Println("Error upserting project:", err)
137	}
138	*manualRefresh <- struct{}{}
139}
140
141func Untrack(dbConn *sql.DB, manualRefresh *chan struct{}, url string) {
142	err := db.DeleteProject(dbConn, url)
143	if err != nil {
144		fmt.Println("Error deleting project:", err)
145	}
146
147	*manualRefresh <- struct{}{}
148
149	err = git.RemoveRepo(url)
150	if err != nil {
151		log.Println(err)
152	}
153}
154
155func RefreshLoop(dbConn *sql.DB, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
156	ticker := time.NewTicker(time.Second * time.Duration(interval))
157
158	fetch := func() []Project {
159		projectsList, err := GetProjects(dbConn)
160		if err != nil {
161			fmt.Println("Error getting projects:", err)
162		}
163		for i, p := range projectsList {
164			p, err := fetchReleases(dbConn, p)
165			if err != nil {
166				fmt.Println(err)
167				continue
168			}
169			projectsList[i] = p
170		}
171		sort.Slice(projectsList, func(i, j int) bool {
172			return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
173		})
174		for i := range projectsList {
175			err = upsert(dbConn, projectsList[i].URL, projectsList[i].Releases)
176			if err != nil {
177				fmt.Println("Error upserting release:", err)
178				continue
179			}
180		}
181		return projectsList
182	}
183
184	projects := fetch()
185
186	for {
187		select {
188		case <-ticker.C:
189			projects = fetch()
190		case <-*manualRefresh:
191			ticker.Reset(time.Second * 3600)
192			projects = fetch()
193		case <-*req:
194			projectsCopy := make([]Project, len(projects))
195			copy(projectsCopy, projects)
196			*res <- projectsCopy
197		}
198	}
199}
200
201// GetProject returns a project from the database
202func GetProject(dbConn *sql.DB, url string) (Project, error) {
203	projectDB, err := db.GetProject(dbConn, url)
204	if err != nil {
205		return Project{}, err
206	}
207	p := Project{
208		URL:     projectDB["url"],
209		Name:    projectDB["name"],
210		Forge:   projectDB["forge"],
211		Running: projectDB["version"],
212	}
213	return p, err
214}
215
216// GetProjects returns a list of all projects from the database
217func GetProjects(dbConn *sql.DB) ([]Project, error) {
218	projectsDB, err := db.GetProjects(dbConn)
219	if err != nil {
220		return nil, err
221	}
222
223	projects := make([]Project, len(projectsDB))
224	for i, p := range projectsDB {
225		projects[i] = Project{
226			URL:     p["url"],
227			Name:    p["name"],
228			Forge:   p["forge"],
229			Running: p["version"],
230		}
231	}
232
233	return projects, nil
234}