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 "git.sr.ht/~amolith/willow/db"
17 "git.sr.ht/~amolith/willow/git"
18 "git.sr.ht/~amolith/willow/rss"
19)
20
21type Project struct {
22 URL string
23 Name string
24 Forge string
25 Running string
26 Releases []Release
27}
28
29type Release struct {
30 URL string
31 Tag string
32 Content string
33 Date time.Time
34}
35
36// GetReleases returns a list of all releases for a project from the database
37func GetReleases(dbConn *sql.DB, proj Project) (Project, error) {
38 ret, err := db.GetReleases(dbConn, proj.URL)
39 if err != nil {
40 return proj, err
41 }
42
43 if len(ret) == 0 {
44 return fetchReleases(dbConn, proj)
45 }
46
47 for _, row := range ret {
48 proj.Releases = append(proj.Releases, Release{
49 Tag: row["tag"],
50 Content: row["content"],
51 URL: row["release_url"],
52 Date: time.Time{},
53 })
54 }
55 sort.Slice(proj.Releases, func(i, j int) bool {
56 return proj.Releases[i].Date.After(proj.Releases[j].Date)
57 })
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 sort.Slice(p.Releases, func(i, j int) bool {
104 return p.Releases[i].Date.After(p.Releases[j].Date)
105 })
106 return p, err
107}
108
109// upsert updates or inserts a project release into the database
110func upsert(dbConn *sql.DB, url string, releases []Release) error {
111 for _, release := range releases {
112 date := release.Date.Format("2006-01-02 15:04:05")
113 idByte := sha256.Sum256([]byte(url + release.URL + release.Tag + date))
114 id := fmt.Sprintf("%x", idByte)
115 err := db.UpsertRelease(dbConn, id, url, release.URL, release.Tag, release.Content, date)
116 if err != nil {
117 log.Printf("Error upserting release: %v", err)
118 return err
119 }
120 }
121 return nil
122}
123
124func Track(dbConn *sql.DB, manualRefresh *chan struct{}, name, url, forge, release string) {
125 err := db.UpsertProject(dbConn, url, name, forge, release)
126 if err != nil {
127 fmt.Println("Error upserting project:", err)
128 }
129 *manualRefresh <- struct{}{}
130}
131
132func Untrack(dbConn *sql.DB, manualRefresh *chan struct{}, url string) {
133 err := db.DeleteProject(dbConn, url)
134 if err != nil {
135 fmt.Println("Error deleting project:", err)
136 }
137
138 *manualRefresh <- struct{}{}
139
140 err = git.RemoveRepo(url)
141 if err != nil {
142 log.Println(err)
143 }
144}
145
146func RefreshLoop(dbConn *sql.DB, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
147 ticker := time.NewTicker(time.Second * time.Duration(interval))
148
149 fetch := func() []Project {
150 projectsList, err := GetProjects(dbConn)
151 if err != nil {
152 fmt.Println("Error getting projects:", err)
153 }
154 for i, p := range projectsList {
155 p, err := fetchReleases(dbConn, p)
156 if err != nil {
157 fmt.Println(err)
158 continue
159 }
160 projectsList[i] = p
161 }
162 sort.Slice(projectsList, func(i, j int) bool {
163 return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
164 })
165 for i := range projectsList {
166 err = upsert(dbConn, projectsList[i].URL, projectsList[i].Releases)
167 if err != nil {
168 fmt.Println("Error upserting release:", err)
169 continue
170 }
171 }
172 return projectsList
173 }
174
175 projects := fetch()
176
177 for {
178 select {
179 case <-ticker.C:
180 projects = fetch()
181 case <-*manualRefresh:
182 ticker.Reset(time.Second * 3600)
183 projects = fetch()
184 case <-*req:
185 projectsCopy := make([]Project, len(projects))
186 copy(projectsCopy, projects)
187 *res <- projectsCopy
188 }
189 }
190}
191
192// GetProject returns a project from the database
193func GetProject(dbConn *sql.DB, url string) (Project, error) {
194 var p Project
195 projectDB, err := db.GetProject(dbConn, url)
196 if err != nil {
197 return p, err
198 }
199 p = Project{
200 URL: projectDB["url"],
201 Name: projectDB["name"],
202 Forge: projectDB["forge"],
203 Running: projectDB["version"],
204 }
205 return p, err
206}
207
208// GetProjects returns a list of all projects from the database
209func GetProjects(dbConn *sql.DB) ([]Project, error) {
210 projectsDB, err := db.GetProjects(dbConn)
211 if err != nil {
212 return nil, err
213 }
214
215 projects := make([]Project, len(projectsDB))
216 for i, p := range projectsDB {
217 projects[i] = Project{
218 URL: p["url"],
219 Name: p["name"],
220 Forge: p["forge"],
221 Running: p["version"],
222 }
223 }
224
225 return projects, nil
226}