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	"git.sr.ht/~amolith/willow/db"
 17	"git.sr.ht/~amolith/willow/git"
 18	"git.sr.ht/~amolith/willow/rss"
 19)
 20
 21type Project struct {
 22	URL      string
 23	Name     string
 24	Forge    string
 25	Running  string
 26	Releases []Release
 27}
 28
 29type Release struct {
 30	URL     string
 31	Tag     string
 32	Content string
 33	Date    time.Time
 34}
 35
 36// GetReleases returns a list of all releases for a project from the database
 37func GetReleases(dbConn *sql.DB, proj Project) (Project, error) {
 38	ret, err := db.GetReleases(dbConn, proj.URL)
 39	if err != nil {
 40		return proj, err
 41	}
 42
 43	if len(ret) == 0 {
 44		return fetchReleases(dbConn, proj)
 45	}
 46
 47	for _, row := range ret {
 48		proj.Releases = append(proj.Releases, Release{
 49			Tag:     row["tag"],
 50			Content: row["content"],
 51			URL:     row["release_url"],
 52			Date:    time.Time{},
 53		})
 54	}
 55	sort.Slice(proj.Releases, func(i, j int) bool {
 56		return proj.Releases[i].Date.After(proj.Releases[j].Date)
 57	})
 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	sort.Slice(p.Releases, func(i, j int) bool {
104		return p.Releases[i].Date.After(p.Releases[j].Date)
105	})
106	return p, err
107}
108
109// upsert updates or inserts a project release into the database
110func upsert(dbConn *sql.DB, url string, releases []Release) error {
111	for _, release := range releases {
112		date := release.Date.Format("2006-01-02 15:04:05")
113		idByte := sha256.Sum256([]byte(url + release.URL + release.Tag + date))
114		id := fmt.Sprintf("%x", idByte)
115		err := db.UpsertRelease(dbConn, id, url, release.URL, release.Tag, release.Content, date)
116		if err != nil {
117			log.Printf("Error upserting release: %v", err)
118			return err
119		}
120	}
121	return nil
122}
123
124func Track(dbConn *sql.DB, manualRefresh *chan struct{}, name, url, forge, release string) {
125	err := db.UpsertProject(dbConn, url, name, forge, release)
126	if err != nil {
127		fmt.Println("Error upserting project:", err)
128	}
129	*manualRefresh <- struct{}{}
130}
131
132func Untrack(dbConn *sql.DB, manualRefresh *chan struct{}, url string) {
133	err := db.DeleteProject(dbConn, url)
134	if err != nil {
135		fmt.Println("Error deleting project:", err)
136	}
137
138	*manualRefresh <- struct{}{}
139
140	err = git.RemoveRepo(url)
141	if err != nil {
142		log.Println(err)
143	}
144}
145
146func RefreshLoop(dbConn *sql.DB, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
147	ticker := time.NewTicker(time.Second * time.Duration(interval))
148
149	fetch := func() []Project {
150		projectsList, err := GetProjects(dbConn)
151		if err != nil {
152			fmt.Println("Error getting projects:", err)
153		}
154		for i, p := range projectsList {
155			p, err := fetchReleases(dbConn, p)
156			if err != nil {
157				fmt.Println(err)
158				continue
159			}
160			projectsList[i] = p
161		}
162		sort.Slice(projectsList, func(i, j int) bool {
163			return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
164		})
165		for i := range projectsList {
166			err = upsert(dbConn, projectsList[i].URL, projectsList[i].Releases)
167			if err != nil {
168				fmt.Println("Error upserting release:", err)
169				continue
170			}
171		}
172		return projectsList
173	}
174
175	projects := fetch()
176
177	for {
178		select {
179		case <-ticker.C:
180			projects = fetch()
181		case <-*manualRefresh:
182			ticker.Reset(time.Second * 3600)
183			projects = fetch()
184		case <-*req:
185			projectsCopy := make([]Project, len(projects))
186			copy(projectsCopy, projects)
187			*res <- projectsCopy
188		}
189	}
190}
191
192// GetProject returns a project from the database
193func GetProject(dbConn *sql.DB, url string) (Project, error) {
194	var p Project
195	projectDB, err := db.GetProject(dbConn, url)
196	if err != nil {
197		return p, err
198	}
199	p = Project{
200		URL:     projectDB["url"],
201		Name:    projectDB["name"],
202		Forge:   projectDB["forge"],
203		Running: projectDB["version"],
204	}
205	return p, err
206}
207
208// GetProjects returns a list of all projects from the database
209func GetProjects(dbConn *sql.DB) ([]Project, error) {
210	projectsDB, err := db.GetProjects(dbConn)
211	if err != nil {
212		return nil, err
213	}
214
215	projects := make([]Project, len(projectsDB))
216	for i, p := range projectsDB {
217		projects[i] = Project{
218			URL:     p["url"],
219			Name:    p["name"],
220			Forge:   p["forge"],
221			Running: p["version"],
222		}
223	}
224
225	return projects, nil
226}