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