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