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