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