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