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 idByte := sha256.Sum256([]byte(url + release.URL + release.Tag + date))
119 id := fmt.Sprintf("%x", idByte)
120 err := db.UpsertRelease(dbConn, id, url, release.URL, release.Tag, release.Content, date)
121 if err != nil {
122 log.Printf("Error upserting release: %v", err)
123 return err
124 }
125 }
126 return nil
127}
128
129func Track(dbConn *sql.DB, manualRefresh *chan struct{}, name, url, forge, release string) {
130 err := db.UpsertProject(dbConn, url, name, forge, release)
131 if err != nil {
132 fmt.Println("Error upserting project:", err)
133 }
134 *manualRefresh <- struct{}{}
135}
136
137func Untrack(dbConn *sql.DB, manualRefresh *chan struct{}, url string) {
138 err := db.DeleteProject(dbConn, url)
139 if err != nil {
140 fmt.Println("Error deleting project:", err)
141 }
142
143 *manualRefresh <- struct{}{}
144
145 err = git.RemoveRepo(url)
146 if err != nil {
147 log.Println(err)
148 }
149}
150
151func RefreshLoop(dbConn *sql.DB, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
152 ticker := time.NewTicker(time.Second * time.Duration(interval))
153
154 fetch := func() []Project {
155 projectsList, err := GetProjects(dbConn)
156 if err != nil {
157 fmt.Println("Error getting projects:", err)
158 }
159 for i, p := range projectsList {
160 p, err := fetchReleases(dbConn, p)
161 if err != nil {
162 fmt.Println(err)
163 continue
164 }
165 projectsList[i] = p
166 }
167 sort.Slice(projectsList, func(i, j int) bool {
168 return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
169 })
170 for i := range projectsList {
171 err = upsert(dbConn, projectsList[i].URL, projectsList[i].Releases)
172 if err != nil {
173 fmt.Println("Error upserting release:", err)
174 continue
175 }
176 }
177 return projectsList
178 }
179
180 projects := fetch()
181
182 for {
183 select {
184 case <-ticker.C:
185 projects = fetch()
186 case <-*manualRefresh:
187 ticker.Reset(time.Second * 3600)
188 projects = fetch()
189 case <-*req:
190 projectsCopy := make([]Project, len(projects))
191 copy(projectsCopy, projects)
192 *res <- projectsCopy
193 }
194 }
195}
196
197// GetProject returns a project from the database
198func GetProject(dbConn *sql.DB, url string) (Project, error) {
199 projectDB, err := db.GetProject(dbConn, url)
200 if err != nil {
201 return Project{}, err
202 }
203 if len(projectDB) == 0 {
204 return Project{}, nil
205 }
206 p := Project{
207 URL: projectDB["url"],
208 Name: projectDB["name"],
209 Forge: projectDB["forge"],
210 Running: projectDB["version"],
211 }
212 return p, err
213}
214
215// GetProjects returns a list of all projects from the database
216func GetProjects(dbConn *sql.DB) ([]Project, error) {
217 projectsDB, err := db.GetProjects(dbConn)
218 if err != nil {
219 return nil, err
220 }
221
222 projects := make([]Project, len(projectsDB))
223 for i, p := range projectsDB {
224 projects[i] = Project{
225 URL: p["url"],
226 Name: p["name"],
227 Forge: p["forge"],
228 Running: p["version"],
229 }
230 }
231
232 return projects, nil
233}