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