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