package main import ( "encoding/json" "fmt" "io" "net/http" "runtime" "sync" "time" ) func errorContext(skip int) string { pc, file, line, ok := runtime.Caller(skip) if !ok { return "unknown" } fn := runtime.FuncForPC(pc).Name() return fmt.Sprintf("%s:%d %s", file, line, fn) } func APITraceError(format string, args ...interface{}) error { traceInfo := errorContext(2) return fmt.Errorf("[%s] "+format, append([]interface{}{traceInfo}, args...)...) } type HypixelPlayerAchievements struct { BedwarsStar int `json:"bedwars_level"` } type HypixelPlayerBedwarsStats struct { Experience int `json:"Experience"` Winstreak int `json:"winstreak"` Wins int `json:"wins_bedwars"` Losses int `json:"losses_bedwars"` GamesPlayed int `json:"games_played_bedwars"` WinLossRatio float32 // wins / gamesplayed Kills int `json:"kills_bedwars"` Deaths int `json:"deaths_bedwars"` KillDeathRatio float32 // kills / deaths FinalKills int `json:"final_kills_bedwars"` FinalDeaths int `json:"final_deaths_bedwars"` FinalKillDeathRatio float32 // final kills / final deaths BedsBroken int `json:"beds_broken_bedwars"` BedsLost int `json:"beds_lost_bedwars"` BedsBrokenLostRatio float32 // beds broken / beds lost } func (stats *HypixelPlayerBedwarsStats) CalculateRatios() { // WLR if stats.GamesPlayed > 0 { stats.WinLossRatio = float32(stats.Wins) / float32(stats.GamesPlayed) } else { stats.WinLossRatio = float32(stats.Wins) } // KDR if stats.Deaths > 0 { stats.KillDeathRatio = float32(stats.Kills) / float32(stats.Deaths) } else { stats.KillDeathRatio = float32(stats.Kills) } // FKDR if stats.FinalDeaths > 0 { stats.FinalKillDeathRatio = float32(stats.FinalKills) / float32(stats.FinalDeaths) } else { stats.FinalKillDeathRatio = float32(stats.FinalKills) } // BBLR if stats.BedsLost > 0 { stats.BedsBrokenLostRatio = float32(stats.BedsBroken) / float32(stats.BedsLost) } else { stats.BedsBrokenLostRatio = float32(stats.BedsBroken) } } func (stats *HypixelPlayerBedwarsStats) IsWinstreakDisabled() bool { return stats.Winstreak == -1 } type HypixelPlayerStats struct { Bedwars HypixelPlayerBedwarsStats `json:"Bedwars"` } type HypixelPlayer struct { DisplayName string `json:"displayname"` Achievements HypixelPlayerAchievements `json:"achievements"` Stats HypixelPlayerStats `json:"stats"` MonthlyRankColor string `json:"monthlyRankColor"` RankPlusColor string `json:"rankPlusColor"` NetworkExp json.Number `json:"networkExp"` } type HypixelPlayerResponse struct { Success bool `json:"success"` Player HypixelPlayer `json:"player"` } type HypixelAPIKey struct { Value string Uses int RateLimit time.Duration LimitUsesLeft int } func NewHypixelAPIKey(key string) *HypixelAPIKey { return &HypixelAPIKey{ Value: key, Uses: 0, RateLimit: time.Millisecond * 1, LimitUsesLeft: -1, } } type HypixelAPI struct { BaseURL string APIKey HypixelAPIKey } func NewHypixelAPI(apiKey string) *HypixelAPI { // TODO: Ensure valid key before creating key := NewHypixelAPIKey(apiKey) return &HypixelAPI{ BaseURL: "https://api.hypixel.net/v2/", APIKey: *key, } } func (h *HypixelAPI) FetchPlayer(PlayerUUID string) (HypixelPlayerResponse, error) { // TODO: Handle nicked players if len(PlayerUUID) != 32 && len(PlayerUUID) != 36 { return HypixelPlayerResponse{}, fmt.Errorf("invalid Player UUID! please pass in a valid UUID (Not a name!)") } RequestUrl := h.BaseURL + "player?uuid=" + PlayerUUID client := &http.Client{ Timeout: 10 * time.Second, } req, err := http.NewRequest("GET", RequestUrl, nil) if err != nil { return HypixelPlayerResponse{}, APITraceError("failed to create HTTP request: %v", err) } req.Header.Set("API-Key", h.APIKey.Value) resp, err := client.Do(req) if err != nil { return HypixelPlayerResponse{}, APITraceError("failed to send HTTP request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return HypixelPlayerResponse{}, APITraceError("received non-200 response: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return HypixelPlayerResponse{}, APITraceError("failed to read response body: %v", err) } var playerResponse HypixelPlayerResponse err = json.Unmarshal(body, &playerResponse) if err != nil { return HypixelPlayerResponse{}, APITraceError("failed to unmarshal JSON: %v", err) } if !playerResponse.Success { return HypixelPlayerResponse{}, APITraceError("API returned unsuccessful response") } playerResponse.Player.Stats.Bedwars.CalculateRatios() return playerResponse, nil } func (h *HypixelAPI) FetchPlayerAsync(PlayerUUID string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) { go func() { result, err := h.FetchPlayer(PlayerUUID) if err != nil { errorChan <- err return } resultChan <- result }() } func (h *HypixelAPI) FetchPlayersAsync(PlayerUUIDs []string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) { var wg sync.WaitGroup for _, uuid := range PlayerUUIDs { wg.Add(1) go func(uuid string) { defer wg.Done() result, err := h.FetchPlayer(uuid) if err != nil { errorChan <- err return } resultChan <- result }(uuid) } go func() { wg.Wait() close(resultChan) close(errorChan) }() }