package main import ( "encoding/json" "fmt" "log" "net/http" "os" "sync" "time" "github.com/joho/godotenv" ) // PunishmentStats mirrors Hypixel's /punishmentstats response. type PunishmentStats struct { Success bool `json:"success"` WatchdogLastMinute int64 `json:"watchdog_lastMinute"` WatchdogRollingDaily int64 `json:"watchdog_rollingDaily"` WatchdogTotal int64 `json:"watchdog_total"` StaffRollingDaily int64 `json:"staff_rollingDaily"` StaffTotal int64 `json:"staff_total"` } type cachedStats struct { stats *PunishmentStats fetchedAt time.Time fetchError error } type server struct { apiKey string client *http.Client cacheTTL time.Duration cacheMu sync.RWMutex cache cachedStats } func newServer(apiKey string, cacheTTL time.Duration) *server { return &server{ apiKey: apiKey, client: &http.Client{Timeout: 10 * time.Second}, cacheTTL: cacheTTL, } } func (s *server) fetchStats() (*PunishmentStats, error) { req, err := http.NewRequest(http.MethodGet, "https://api.hypixel.net/v2/punishmentstats", nil) if err != nil { return nil, err } req.Header.Set("API-Key", s.apiKey) resp, err := s.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("bad status: %s", resp.Status) } var stats PunishmentStats if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil { return nil, err } if !stats.Success { return nil, fmt.Errorf("hypixel api reported failure") } return &stats, nil } func (s *server) getStats() (*PunishmentStats, error) { s.cacheMu.RLock() cache := s.cache s.cacheMu.RUnlock() if cache.stats != nil && time.Since(cache.fetchedAt) < s.cacheTTL { return cache.stats, nil } s.cacheMu.Lock() defer s.cacheMu.Unlock() // Double-check after acquiring write lock. if s.cache.stats != nil && time.Since(s.cache.fetchedAt) < s.cacheTTL { return s.cache.stats, nil } stats, err := s.fetchStats() s.cache = cachedStats{stats: stats, fetchedAt: time.Now(), fetchError: err} if err != nil { return nil, err } return stats, nil } func (s *server) handleStats(w http.ResponseWriter, r *http.Request) { stats, err := s.getStats() if err != nil { http.Error(w, fmt.Sprintf(`{"error":%q}`, err.Error()), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") if err := json.NewEncoder(w).Encode(stats); err != nil { log.Printf("encode response: %v", err) } } func handleHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{"status":"ok"}`) } func main() { _ = godotenv.Load() apiKey := os.Getenv("HYPIXEL_API_KEY") if apiKey == "" { log.Fatal("HYPIXEL_API_KEY is required") } port := os.Getenv("PORT") if port == "" { port = "8080" } srv := newServer(apiKey, 10*time.Second) http.HandleFunc("/health", handleHealth) http.HandleFunc("/api/stats", srv.handleStats) log.Printf("api listening on :%s", port) if err := http.ListenAndServe(":"+port, nil); err != nil { log.Fatalf("server error: %v", err) } }