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)) }