reforger-update-api/main.go
2025-06-19 21:16:14 -04:00

145 lines
3.6 KiB
Go

package main
import (
"encoding/json"
"log"
"net/http"
"regexp"
"sync"
"time"
"github.com/mmcdole/gofeed"
"github.com/spf13/viper"
)
type Update struct {
Version string `json:"version"`
Build string `json:"build"`
Published string `json:"published"` // ISO date string YYYY-MM-DD
}
var (
cacheMu sync.Mutex
cachedUpdates []Update
cacheExpiry time.Time
cacheDuration time.Duration
)
func fetchUpdates(feedURL string) ([]Update, error) {
parser := gofeed.NewParser()
feed, err := parser.ParseURL(feedURL)
if err != nil {
return nil, err
}
versionRe := regexp.MustCompile(`(?i)(?:^|\s)(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)(?:\s+)?Update|Update\s+(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)`)
buildRe := regexp.MustCompile(`SteamDB Build (\d+)`)
var updates []Update
for _, item := range feed.Items {
buildMatch := buildRe.FindStringSubmatch(item.Description)
versionMatch := versionRe.FindStringSubmatch(item.Description)
if len(buildMatch) > 1 && len(versionMatch) > 1 {
version := versionMatch[1]
if version == "" && len(versionMatch) > 2 {
version = versionMatch[2]
}
build := buildMatch[1]
pubTime, err := time.Parse(time.RFC1123Z, item.Published)
if err != nil {
log.Printf("Error parsing date for item %s: %v", item.Title, err)
continue
}
updates = append(updates, Update{
Version: version,
Build: build,
Published: pubTime.Format("2006-01-02"),
})
}
}
return updates, nil
}
func getCachedUpdates(feedURL string) ([]Update, error) {
cacheMu.Lock()
defer cacheMu.Unlock()
if time.Now().Before(cacheExpiry) && cachedUpdates != nil {
return cachedUpdates, nil
}
updates, err := fetchUpdates(feedURL)
if err != nil {
return nil, err
}
cachedUpdates = updates
cacheExpiry = time.Now().Add(cacheDuration)
return updates, nil
}
func invalidateCache(w http.ResponseWriter, r *http.Request) {
cacheMu.Lock()
defer cacheMu.Unlock()
cachedUpdates = nil
cacheExpiry = time.Time{}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Cache invalidated\n"))
}
func updatesHandler(w http.ResponseWriter, r *http.Request) {
const feedURL = "https://steamdb.info/api/PatchnotesRSS/?appid=1874880"
updates, err := getCachedUpdates(feedURL)
if err != nil {
http.Error(w, "Failed to fetch updates: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(updates); err != nil {
http.Error(w, "Failed to encode JSON: "+err.Error(), http.StatusInternalServerError)
}
}
func main() {
// Setup Viper
viper.SetDefault("port", "8080")
viper.SetDefault("cache_duration", "5m") // 5 minutes
viper.SetConfigName("config") // config file name (without extension)
viper.SetConfigType("yaml") // or json, toml, etc.
viper.AddConfigPath(".") // look for config in working directory
// Read config file (if exists)
err := viper.ReadInConfig()
if err != nil {
log.Println("No config file found or error reading config, continuing with defaults/env")
}
// Allow environment variables to override config
viper.AutomaticEnv() // read env variables matching keys (e.g., PORT, CACHE_DURATION)
// Parse cache duration
cacheDurationStr := viper.GetString("cache_duration")
dur, err := time.ParseDuration(cacheDurationStr)
if err != nil {
log.Fatalf("Invalid cache_duration: %v", err)
}
cacheDuration = dur
port := viper.GetString("port")
http.HandleFunc("/updates", updatesHandler)
http.HandleFunc("/invalidate-cache", invalidateCache)
log.Printf("Listening on http://localhost:%s/", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}