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 "errors"
11 "fmt"
12 "log"
13 "sort"
14 "strings"
15 "sync"
16 "time"
17
18 "github.com/unascribed/FlexVer/go/flexver"
19
20 "git.sr.ht/~amolith/willow/db"
21 "git.sr.ht/~amolith/willow/git"
22 "git.sr.ht/~amolith/willow/rss"
23)
24
25type Project struct {
26 ID string
27 URL string
28 Name string
29 Forge string
30 Running string
31 Releases []Release
32}
33
34type Release struct {
35 ID string
36 ProjectID string
37 URL string
38 Tag string
39 Content string
40 Date time.Time
41}
42
43// GetReleases returns a list of all releases for a project from the database
44func GetReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
45 proj.ID = GenProjectID(proj.URL, proj.Name, proj.Forge)
46
47 ret, err := db.GetReleases(dbConn, proj.ID)
48 if err != nil {
49 return proj, err
50 }
51
52 if len(ret) == 0 {
53 proj, err = fetchReleases(dbConn, mu, proj)
54 if err != nil {
55 return proj, err
56 }
57 err = upsertReleases(dbConn, mu, proj.ID, proj.Releases)
58 if err != nil {
59 return proj, err
60 }
61 return proj, nil
62 }
63
64 for _, row := range ret {
65 proj.Releases = append(proj.Releases, Release{
66 ID: row["id"],
67 ProjectID: proj.ID,
68 Tag: row["tag"],
69 Content: row["content"],
70 URL: row["release_url"],
71 Date: time.Time{},
72 })
73 }
74 proj.Releases = SortReleases(proj.Releases)
75 return proj, nil
76}
77
78// fetchReleases fetches releases from a project's forge given its URI
79func fetchReleases(dbConn *sql.DB, mu *sync.Mutex, p Project) (Project, error) {
80 var err error
81 switch p.Forge {
82 case "github", "gitea", "forgejo":
83 rssReleases, err := rss.GetReleases(p.URL)
84 if err != nil {
85 fmt.Println("Error getting RSS releases:", err)
86 return p, err
87 }
88 for _, release := range rssReleases {
89 p.Releases = append(p.Releases, Release{
90 ID: GenReleaseID(p.URL, release.URL, release.Tag),
91 Tag: release.Tag,
92 Content: release.Content,
93 URL: release.URL,
94 Date: release.Date,
95 })
96 err = upsertReleases(dbConn, mu, p.ID, p.Releases)
97 if err != nil {
98 log.Printf("Error upserting release: %v", err)
99 return p, err
100 }
101 }
102 default:
103 gitReleases, err := git.GetReleases(p.URL, p.Forge)
104 if err != nil {
105 return p, err
106 }
107 for _, release := range gitReleases {
108 p.Releases = append(p.Releases, Release{
109 ID: GenReleaseID(p.URL, release.URL, release.Tag),
110 Tag: release.Tag,
111 Content: release.Content,
112 URL: release.URL,
113 Date: release.Date,
114 })
115 err = upsertReleases(dbConn, mu, p.ID, p.Releases)
116 if err != nil {
117 log.Printf("Error upserting release: %v", err)
118 return p, err
119 }
120 }
121 }
122 p.Releases = SortReleases(p.Releases)
123 return p, err
124}
125
126func SortReleases(releases []Release) []Release {
127 sort.Slice(releases, func(i, j int) bool {
128 return !flexver.Less(releases[i].Tag, releases[j].Tag)
129 })
130 return releases
131}
132
133func SortProjects(projects []Project) []Project {
134 sort.Slice(projects, func(i, j int) bool {
135 return strings.ToLower(projects[i].Name) < strings.ToLower(projects[j].Name)
136 })
137 return projects
138}
139
140// upsertReleases updates or inserts a release in the database
141func upsertReleases(dbConn *sql.DB, mu *sync.Mutex, projID string, releases []Release) error {
142 for _, release := range releases {
143 date := release.Date.Format("2006-01-02 15:04:05")
144 err := db.UpsertRelease(dbConn, mu, release.ID, projID, release.URL, release.Tag, release.Content, date)
145 if err != nil {
146 log.Printf("Error upserting release: %v", err)
147 return err
148 }
149 }
150 return nil
151}
152
153// GenReleaseID generates a likely-unique ID from its project's URL, its release's URL, and its tag
154func GenReleaseID(projectURL, releaseURL, tag string) string {
155 idByte := sha256.Sum256([]byte(projectURL + releaseURL + tag))
156 return fmt.Sprintf("%x", idByte)
157}
158
159// GenProjectID generates a likely-unique ID from a project's URI, name, and forge
160func GenProjectID(url, name, forge string) string {
161 idByte := sha256.Sum256([]byte(url + name + forge))
162 return fmt.Sprintf("%x", idByte)
163}
164
165func Track(dbConn *sql.DB, mu *sync.Mutex, manualRefresh *chan struct{}, name, url, forge, release string) {
166 id := GenProjectID(url, name, forge)
167 err := db.UpsertProject(dbConn, mu, id, url, name, forge, release)
168 if err != nil {
169 fmt.Println("Error upserting project:", err)
170 }
171 *manualRefresh <- struct{}{}
172}
173
174func Untrack(dbConn *sql.DB, mu *sync.Mutex, id string) {
175 proj, err := db.GetProject(dbConn, id)
176 if err != nil {
177 fmt.Println("Error getting project:", err)
178 }
179
180 err = db.DeleteProject(dbConn, mu, proj["id"])
181 if err != nil {
182 fmt.Println("Error deleting project:", err)
183 }
184
185 // TODO: before removing, check whether other tracked projects use the same
186 // repo
187 err = git.RemoveRepo(proj["url"])
188 if err != nil {
189 log.Println(err)
190 }
191}
192
193func RefreshLoop(dbConn *sql.DB, mu *sync.Mutex, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
194 ticker := time.NewTicker(time.Second * time.Duration(interval))
195
196 fetch := func() []Project {
197 projectsList, err := GetProjects(dbConn)
198 if err != nil {
199 fmt.Println("Error getting projects:", err)
200 }
201 for i, p := range projectsList {
202 p, err := fetchReleases(dbConn, mu, p)
203 if err != nil {
204 fmt.Println(err)
205 continue
206 }
207 projectsList[i] = p
208 }
209 sort.Slice(projectsList, func(i, j int) bool {
210 return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
211 })
212 for i := range projectsList {
213 err = upsertReleases(dbConn, mu, projectsList[i].ID, projectsList[i].Releases)
214 if err != nil {
215 fmt.Println("Error upserting release:", err)
216 continue
217 }
218 }
219 return projectsList
220 }
221
222 projects := fetch()
223
224 for {
225 select {
226 case <-ticker.C:
227 projects = fetch()
228 case <-*manualRefresh:
229 ticker.Reset(time.Second * 3600)
230 projects = fetch()
231 case <-*req:
232 projectsCopy := make([]Project, len(projects))
233 copy(projectsCopy, projects)
234 *res <- projectsCopy
235 }
236 }
237}
238
239// GetProject returns a project from the database
240func GetProject(dbConn *sql.DB, proj Project) (Project, error) {
241 projectDB, err := db.GetProject(dbConn, proj.ID)
242 if err != nil && errors.Is(err, sql.ErrNoRows) {
243 return proj, nil
244 } else if err != nil {
245 return proj, err
246 }
247 p := Project{
248 ID: proj.ID,
249 URL: proj.URL,
250 Name: proj.Name,
251 Forge: proj.Forge,
252 Running: projectDB["version"],
253 }
254 return p, err
255}
256
257// GetProjectWithReleases returns a single project from the database along with its releases
258func GetProjectWithReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
259 project, err := GetProject(dbConn, proj)
260 if err != nil {
261 return Project{}, err
262 }
263
264 return GetReleases(dbConn, mu, project)
265}
266
267// GetProjects returns a list of all projects from the database
268func GetProjects(dbConn *sql.DB) ([]Project, error) {
269 projectsDB, err := db.GetProjects(dbConn)
270 if err != nil {
271 return nil, err
272 }
273
274 projects := make([]Project, len(projectsDB))
275 for i, p := range projectsDB {
276 projects[i] = Project{
277 ID: p["id"],
278 URL: p["url"],
279 Name: p["name"],
280 Forge: p["forge"],
281 Running: p["version"],
282 }
283 }
284
285 return SortProjects(projects), nil
286}
287
288// GetProjectsWithReleases returns a list of all projects and all their releases
289// from the database
290func GetProjectsWithReleases(dbConn *sql.DB, mu *sync.Mutex) ([]Project, error) {
291 projects, err := GetProjects(dbConn)
292 if err != nil {
293 return nil, err
294 }
295
296 for i := range projects {
297 projects[i], err = GetReleases(dbConn, mu, projects[i])
298 if err != nil {
299 return nil, err
300 }
301 projects[i].Releases = SortReleases(projects[i].Releases)
302 }
303
304 return SortProjects(projects), nil
305}