1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: Apache-2.0
  4
  5package main
  6
  7import (
  8	"encoding/csv"
  9	"errors"
 10	"fmt"
 11	"log"
 12	"net/http"
 13	"os"
 14	"sort"
 15	"strings"
 16	"time"
 17
 18	"github.com/microcosm-cc/bluemonday"
 19)
 20
 21type (
 22	Model struct {
 23		Projects []project
 24	}
 25
 26	project struct {
 27		URL      string
 28		Name     string
 29		Forge    string
 30		Running  string
 31		Releases []release
 32	}
 33
 34	release struct {
 35		Tag     string
 36		Content string
 37		URL     string
 38		Date    time.Time
 39	}
 40)
 41
 42var (
 43	req           = make(chan struct{})
 44	manualRefresh = make(chan struct{})
 45	res           = make(chan []project)
 46	m             = Model{
 47		Projects: []project{},
 48	}
 49	bmUGC    = bluemonday.UGCPolicy()
 50	bmStrict = bluemonday.StrictPolicy()
 51)
 52
 53func main() {
 54	file, err := os.Open("projects.csv")
 55	if err != nil {
 56		if os.IsNotExist(err) {
 57			file, err = os.Create("projects.csv")
 58			if err != nil {
 59				log.Fatalln(err)
 60			}
 61			defer file.Close()
 62
 63			_, err = file.WriteString("url,name,forge,running\nhttps://git.sr.ht/~amolith/earl,earl,sourcehut,v0.0.1-rc0\n")
 64			if err != nil {
 65				log.Fatalln(err)
 66			}
 67		} else {
 68			log.Fatalln(err)
 69		}
 70	}
 71	defer file.Close()
 72
 73	reader := csv.NewReader(file)
 74
 75	records, err := reader.ReadAll()
 76	if err != nil {
 77		log.Fatalln(err)
 78	}
 79
 80	m.Projects = []project{}
 81	if len(records) > 0 {
 82		for i, record := range records {
 83			if i == 0 {
 84				continue
 85			}
 86			m.Projects = append(m.Projects, project{
 87				URL:      record[0],
 88				Name:     record[1],
 89				Forge:    record[2],
 90				Running:  record[3],
 91				Releases: []release{},
 92			})
 93		}
 94	}
 95
 96	go refreshLoop(manualRefresh, req, res)
 97
 98	mux := http.NewServeMux()
 99
100	httpServer := &http.Server{
101		Addr:    "0.0.0.0:1337",
102		Handler: mux,
103	}
104
105	mux.HandleFunc("/", rootHandler)
106	mux.HandleFunc("/static", staticHandler)
107	mux.HandleFunc("/new", newHandler)
108
109	if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) {
110		log.Println("Web server closed")
111	} else {
112		log.Fatalln(err)
113	}
114}
115
116func refreshLoop(manualRefresh, req chan struct{}, res chan []project) {
117	ticker := time.NewTicker(time.Second * 3600)
118
119	fetch := func() []project {
120		projects := make([]project, len(m.Projects))
121		copy(projects, m.Projects)
122		for i, project := range projects {
123			project, err := getReleases(project)
124			if err != nil {
125				fmt.Println(err)
126				continue
127			}
128			projects[i] = project
129		}
130		sort.Slice(projects, func(i, j int) bool { return strings.ToLower(projects[i].Name) < strings.ToLower(projects[j].Name) })
131		return projects
132	}
133
134	projects := fetch()
135
136	for {
137		select {
138		case <-ticker.C:
139			projects = fetch()
140		case <-manualRefresh:
141			ticker.Reset(time.Second * 3600)
142			projects = fetch()
143		case <-req:
144			projectsCopy := make([]project, len(projects))
145			copy(projectsCopy, projects)
146			res <- projectsCopy
147		}
148	}
149}