package main import ( "bufio" "encoding/binary" "encoding/json" "errors" "fmt" "github.com/google/uuid" "io" "io/ioutil" "log" "net" "net/http" "os" "os/exec" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" "time" ) var key = "ccebff0f-939a-4afe-b5b3-30a7a665ee38" func main() { var demoApp = NewDemoApp(key) demoApp.Start() } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // LogBuf // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - type LogBuffer struct { strings []string size int } func NewLogBuffer(size int) *LogBuffer { return &LogBuffer{ strings: make([]string, 0, size), size: size, } } func (l *LogBuffer) Add(s string) { if len(l.strings) == l.size { l.strings = l.strings[1:] } l.strings = append(l.strings, s) } func (l *LogBuffer) Get() []string { return l.strings } func (l *LogBuffer) GetLast() (string, error) { if len(l.strings) == 0 { return "", fmt.Errorf("log buffer is empty") } return l.strings[len(l.strings)-1], nil } func (l *LogBuffer) GetSecondToLast() (string, error) { if len(l.strings) < 2 { return "", fmt.Errorf("log buffer does not have enough lines") } return l.strings[len(l.strings)-2], nil } func (l *LogBuffer) GetLineStepsBack(x int) (string, error) { if x < 0 || x >= len(l.strings) { return "", fmt.Errorf("log buffer does not have enough lines to step back %d times", x) } return l.strings[len(l.strings)-1-x], nil } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Custom Logger // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var AppLogger = NewCustomLogger() // CustomLogger wraps the standard logger type CustomLogger struct { logger *log.Logger } // NewCustomLogger initializes the custom logger func NewCustomLogger() *CustomLogger { // Create a logger that writes to stdout with no flags return &CustomLogger{ logger: log.New(os.Stdout, "", 0), // we will handle formatting manually } } // logFormat retrieves the function name, file, and line number func logFormat() string { // Use the runtime.Caller to retrieve caller info pc, file, line, ok := runtime.Caller(2) if !ok { file = "unknown" line = 0 } // Get function name funcName := runtime.FuncForPC(pc).Name() // Format timestamp timestamp := time.Now().Format("2006-01-02 15:04:05") // Return formatted log prefix (timestamp, file, line, and function name) return fmt.Sprintf("%s - %s:%d - %s: ", timestamp, file, line, funcName) } // Info logs informational messages func (c *CustomLogger) Info(msg string) { c.logger.Println(logFormat() + "INFO: " + msg) } // Error logs error messages func (c *CustomLogger) Error(msg string) { c.logger.Println(logFormat() + "ERROR: " + msg) } // Example usage of the custom logger func showcase() { logger := NewCustomLogger() logger.Info("This is an info message") logger.Error("This is an error message") } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Main // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - type PlayerWrapper struct { Player Player `json:"player"` } func replaceCorruptedRune(msg string) string { runes := []rune(msg) for i, r := range runes { if r == '�' { runes[i] = '§' } } return string(runes) } func clearTerminal() { var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("cmd", "/c", "cls") } else { cmd = exec.Command("clear") } cmd.Stdout = os.Stdout cmd.Run() } func calcRatio(numerator, denominator int) float64 { if denominator == 0 { return float64(numerator) } return float64(numerator) / float64(denominator) } type DemoApp struct { Client *Client API *HypixelApi MemCache *MemoryCache LogBuf *LogBuffer PartyBuilder []map[string]interface{} } func NewDemoApp(key string) *DemoApp { var api_key = NewAPIKey(key) app := &DemoApp{ API: NewAPI(*api_key), MemCache: &MemoryCache{}, LogBuf: NewLogBuffer(10), PartyBuilder: []map[string]interface{}{}, } app.MemCache.Init() return app } func (app *DemoApp) FetchMCPlayer(name string) (*FetchedPlayerResult, error) { asyncFetcher := NewPlayerFetcher( name, app.MemCache, 2, 2*time.Second, 5*time.Second, ) data, err := asyncFetcher.FetchPlayerData() if err != nil { return nil, err } return data, nil } func (app *DemoApp) onFileEmit(line string) { msg := strings.TrimSpace(line) if len(msg) < 34 { return } submsg := msg[33:] if len(submsg) != 0 { app.LogBuf.Add(submsg) } OnlinePrefix := "[CHAT] ONLINE: " PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------" //PartyMemberCountPrefix := "[CHAT] Party Members (" PartyLeaderPrefix := "[CHAT] Party Leader: " PartyListMembersPrefix := "[CHAT] Party Members: " if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix) players := strings.Split(newsubmsg, ",") var online []CacheResult for _, player := range players { playerName := strings.TrimSpace(player) plr, err := app.FetchMCPlayer(playerName) res_name := plr.Name res_uuid := plr.UUID if err != nil { fmt.Println(fmt.Sprintf("Error fetching UUID: %v", err)) continue } fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid) res_player := CacheResult{ UUID: plr.UUID, Name: plr.Name, } online = append(online, res_player) //names, err := GetNameFromUUID(playerUUID) //if err != nil { // log.Fatalf("Error fetching names from UUID: %v", err) //} //fmt.Printf("Name history for UUID %s: %v\n", playerUUID, names) } app.sendPartyList(online) } else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List last, _ := app.LogBuf.GetSecondToLast() // TODO: Check if moderators if !strings.HasPrefix(last, PartyListMembersPrefix) { return } PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix) var ppl []CacheResult for _, player := range strings.Split(PartyMembersMsg, ",") { playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?")) if strings.HasPrefix(playerName, "[") { playerName = strings.Split(playerName, " ")[1] } plr, err := app.FetchMCPlayer(playerName) if err != nil { log.Fatalf("Error fetching Player: %v", err) continue } res_name := plr.Name res_uuid := plr.UUID res_player := CacheResult{ UUID: plr.UUID, Name: plr.Name, } fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid) ppl = append(ppl, res_player) //playerName := strings.TrimSpace(player) //playerUUID, err := GetUUIDFromName(playerName) //if err != nil { // log.Fatalf("Error fetching UUID: %v", err) // return //} //fmt.Printf("UUID of player %s: %s\n", playerName, playerUUID) //cachedPlayer := CachedUuid{playerUUID, playerName, playerName, time.Now()} //UuidCache.Add(&cachedPlayer) } // Parse Party Leader leaders_msg, err := app.LogBuf.GetLineStepsBack(2) if err != nil { println("Unable to find party leader message") return } PartyLeaderMsg := strings.TrimPrefix(leaders_msg, PartyLeaderPrefix) playerName := strings.TrimSpace(strings.TrimSuffix(PartyLeaderMsg, " ?")) if strings.HasPrefix(playerName, "[") { playerName = strings.Split(playerName, " ")[1] } plr, err := app.FetchMCPlayer(playerName) if err != nil { log.Fatalf("Error fetching Player: %v", err) } res_name := plr.Name res_uuid := plr.UUID res_player := CacheResult{ UUID: plr.UUID, Name: plr.Name, } fmt.Printf("UUID of player %s: %s\n", res_name, res_uuid) ppl = append(ppl, res_player) // Parse Party Count //party_count_msg, err := LogBuf.GetLineStepsBack(4) //if err != nil { // println("Unable to find party count message") // return //} //PartyCountMsg := strings.TrimPrefix(party_count_msg, PartyMemberCountPrefix) //count_str := strings.TrimSuffix(PartyCountMsg, ")") //count, err := strconv.Atoi(count_str) //if err != nil { // println("Unable to parse party count message - Invalid number used") // return //} //print("Expected ") //print(count) //print(" party members\n") app.sendPartyList(ppl) return } println(submsg) } func (app *DemoApp) tailFile(path string, lineCh chan<- string) { file, err := os.Open(path) if err != nil { log.Fatalf("Failed to open file: %v", err) } defer file.Close() file.Seek(0, 2) reader := bufio.NewReader(file) for { line, err := reader.ReadString('\n') if err != nil { time.Sleep(100 * time.Millisecond) continue } lineCh <- line } } func (app *DemoApp) sendPartyList(ppl []CacheResult) { for _, user := range ppl { res, err := app.API.GetPlayerResponse(user.UUID) if err != nil { log.Fatalf("Failed to get player data: %v", err) } res.Player.Stats.Bedwars.WLR = calcRatio(res.Player.Stats.Bedwars.Wins, res.Player.Stats.Bedwars.Losses) res.Player.Stats.Bedwars.KDR = calcRatio(res.Player.Stats.Bedwars.Kills, res.Player.Stats.Bedwars.Deaths) res.Player.Stats.Bedwars.FKDR = calcRatio(res.Player.Stats.Bedwars.FinalKills, res.Player.Stats.Bedwars.FinalDeaths) res.Player.Stats.Bedwars.BBLR = calcRatio(res.Player.Stats.Bedwars.BedsBroken, res.Player.Stats.Bedwars.BedsLost) playerJSON, err := json.Marshal(res) if err != nil { log.Fatalf("Failed to marshal player data: %v", err) } var playerMap map[string]interface{} json.Unmarshal(playerJSON, &playerMap) app.PartyBuilder = append(app.PartyBuilder, playerMap) message, err := json.Marshal(app.PartyBuilder) if err != nil { log.Fatalf("Failed to marshal stuff: %v", err) } err = app.Client.SendData(string(message)) if err != nil { log.Printf("Error sending data: %v", err) } fmt.Println("Sent stuff:", app.PartyBuilder) println("Sending Done!") } // Clear buffer so it only sends the current party list, not previous party lists app.PartyBuilder = []map[string]interface{}{} } func (app *DemoApp) Start() { var cmd string fmt.Print(" | CREATE\n | JOIN\nEnter Choice:\n>") fmt.Scanln(&cmd) if cmd != "CREATE" && cmd != "JOIN" { fmt.Println("Invalid command.") return } var err error app.Client, err = NewClient("chat.itzilly.com", uuid.New().String()) if err != nil { log.Fatalf("Failed to create client: %v", err) return } if cmd == "CREATE" { app.CreateRoom() } else if cmd == "JOIN" { err := app.JoinRoom() if err != nil { return } } fmt.Printf("[DEV] Joined Branches\n") app.Client.ListenForData() for { select { case data, ok := <-app.Client.DataChannel: if !ok { fmt.Println("Data channel closed, exiting...") return } app.HandleData(data) } } fmt.Println("Closing app") } func (app *DemoApp) CreateRoom() { var err error code, err := app.Client.CreateRoom("") if err != nil { log.Fatal(err) return } fmt.Println("Created room:", code) err = app.Client.JoinRoom(code, "password") if err != nil { log.Fatal(err) return } println("Connected to room") path := os.Getenv("USERPROFILE") logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log") fmt.Println("Reading log file from:", logPath) lineCh := make(chan string) go app.tailFile(logPath, lineCh) // TODO: Do this in a different goroutine so that you can still listen to data from your own client for { select { case line := <-lineCh: app.onFileEmit(replaceCorruptedRune(line)) } } } func (app *DemoApp) JoinRoom() error { var code string var password string fmt.Print("Enter Room Code:\n>") fmt.Scanln(&code) fmt.Print("Enter Room Password:\n>") fmt.Scanln(&password) err := app.Client.JoinRoom(code, password) if err != nil { log.Fatal(err) return err } fmt.Println("Joined room:", code) return nil } func (app *DemoApp) HandleData(data string) { var playerWrappers []PlayerWrapper err := json.Unmarshal([]byte(data), &playerWrappers) if err != nil { fmt.Println("Error unmarshalling data:", err) return } var players []*Player for _, wrapper := range playerWrappers { players = append(players, &wrapper.Player) } app.DisplayPlayers(players) } func (app *DemoApp) DisplayPlayers(players []*Player) { clearTerminal() fmt.Printf("| %-20s | %-10s | %-10s |\n", "Player Name", "Bedwars Level", "FKDR") fmt.Println("|----------------------|------------|------------|") for _, player := range players { fmt.Printf("| %-20s | %-10d | %10.3f |\n", player.DisplayName, player.Achievements.BedwarsLevel, player.Stats.Bedwars.FKDR) } fmt.Println("|----------------------|------------|------------|") } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // client // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const PORT = ":5518" type PacketID int const ( CONNECT_REQUEST PacketID = iota CONNECT_RESPONSE JOIN_REQUEST JOIN_RESPONSE CREATE_REQUEST CREATE_RESPONSE READ_DATA SEND_DATA_REQUEST SEND_DATA_RESPONSE LEAVE_ROOM ) // Client represents a client connection to the server. type Client struct { Conn net.Conn ClientID string RoomCode string DataChannel chan string } // NewClient creates a new client and connects to the server. func NewClient(serverAddr, clientID string) (*Client, error) { conn, err := net.Dial("tcp", serverAddr+PORT) if err != nil { return nil, fmt.Errorf("failed to connect to server: %w", err) } return &Client{ Conn: conn, ClientID: clientID, DataChannel: make(chan string), }, nil } // CreateRoom creates a new room on the server. func (c *Client) CreateRoom(password string) (string, error) { request := CreateRoomRequestPacket{ UserID: c.ClientID, Password: password, } if err := c.sendPacket(CREATE_REQUEST, request); err != nil { return "", err } var response CreateRoomResponsePacket if err := c.receivePacket(CREATE_RESPONSE, &response); err != nil { return "", err } if !response.Success { return "", errors.New(response.Reason) } c.RoomCode = response.RoomCode return response.RoomCode, nil } // JoinRoom joins an existing room on the server. func (c *Client) JoinRoom(roomCode, password string) error { request := JoinRequestPacket{ UserID: c.ClientID, RoomCode: roomCode, Password: password, } if err := c.sendPacket(JOIN_REQUEST, request); err != nil { return err } var response JoinRequestResponsePacket if err := c.receivePacket(JOIN_RESPONSE, &response); err != nil { return err } if !response.Success { return errors.New(response.Reason) } c.RoomCode = roomCode return nil } func (c *Client) ListenForData() { go func() { for { var dataPacket SendDataRequestPacket err := c.receivePacket(READ_DATA, &dataPacket) if err != nil { if err == io.EOF { fmt.Println("Connection closed by the server") close(c.DataChannel) return } fmt.Printf("Error receiving data: %v\n", err) close(c.DataChannel) return } // Send the received data to the channel c.DataChannel <- dataPacket.Data } }() } // SendData sends a message to all other clients in the room. func (c *Client) SendData(data string) error { request := SendDataRequestPacket{ UserID: c.ClientID, Data: data, } if err := c.sendPacket(SEND_DATA_REQUEST, request); err != nil { return fmt.Errorf("failed to send data packet: %w", err) } var response SendDataResponsePacket if err := c.receivePacket(SEND_DATA_RESPONSE, &response); err != nil { return fmt.Errorf("failed to receive response for sent data: %w", err) } if !response.Success { return errors.New("server failed to process the data request: " + response.Reason) } return nil } // LeaveRoom disconnects the client from the current room. func (c *Client) LeaveRoom() error { return c.sendPacket(LEAVE_ROOM, nil) } // Close closes the connection to the server. func (c *Client) Close() { c.Conn.Close() } // sendPacket sends a packet with a given ID and data to the server. func (c *Client) sendPacket(packetID PacketID, data interface{}) error { packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to encode packet: %w", err) } return writePacket(c.Conn, byte(packetID), packetData) } // receivePacket reads a response from the server for a given packet ID. func (c *Client) receivePacket(expected PacketID, v interface{}) error { packetID, data, err := readPacket(c.Conn) if err != nil { return err } if packetID != expected { return fmt.Errorf("unexpected packet ID: got %v, want %v", packetID, expected) } return json.Unmarshal(data, v) } // readPacket reads a packet from the connection. func readPacket(conn net.Conn) (PacketID, []byte, error) { var length uint32 if err := binary.Read(conn, binary.BigEndian, &length); err != nil { return 0, nil, err } var messageType byte if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil { return 0, nil, err } data := make([]byte, length-5) if _, err := io.ReadFull(conn, data); err != nil { return 0, nil, err } return PacketID(messageType), data, nil } // writePacket writes a packet to the connection. func writePacket(conn net.Conn, messageType byte, data []byte) error { length := uint32(5 + len(data)) if err := binary.Write(conn, binary.BigEndian, length); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, messageType); err != nil { return err } _, err := conn.Write(data) return err } type CreateRoomRequestPacket struct { UserID string Password string } type CreateRoomResponsePacket struct { Success bool Reason string RoomCode string } type JoinRequestPacket struct { UserID string RoomCode string Password string } type JoinRequestResponsePacket struct { Success bool Reason string CurrentData string } type SendDataRequestPacket struct { UserID string Data string } type SendDataResponsePacket struct { Success bool Reason string } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // config // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - type FileConfig struct { Path string Name string Type string } func GetDefaultConfigPath() string { var filePath string switch os_name := runtime.GOOS; os_name { case "windows": appData := os.Getenv("APPDATA") filePath = path.Join(appData, "hudly", ".conf") case "darwin": homePath := os.Getenv("HOME") filePath = path.Join(homePath, "Library", "Application Support", "hudly", ".conf") case "linux": homePath := os.Getenv("HOME") filePath = path.Join(homePath, ".config", "hudly", ".conf") default: panic("unknown operating system") } return filePath } type Config struct { ConfigVersion int PlayerName string PlayerUUID string AltNames []string AltUUIDs []string AppFeaturesConfig AppFeaturesConfig } type AppFeaturesConfig struct { CheckGuildChat bool CheckOfficerChat bool CheckPartyChat bool CheckPartyList bool CheckLobbyMessages bool CheckQueueMessages bool CheckInGameMessages bool } func GetDefaultConfig() *Config { return &Config{ ConfigVersion: 1, PlayerName: "", PlayerUUID: "", AltNames: []string{}, AltUUIDs: []string{}, AppFeaturesConfig: AppFeaturesConfig{ CheckGuildChat: true, CheckOfficerChat: false, CheckPartyChat: true, CheckPartyList: true, CheckLobbyMessages: false, CheckQueueMessages: true, CheckInGameMessages: true, }, } } // SaveConfig saves the given config struct to the file in JSON format func SaveConfig(config *Config, filePath string) error { // Convert the config struct to JSON data, err := json.MarshalIndent(config, "", " ") if err != nil { return fmt.Errorf("failed to serialize config: %v", err) } // Write the JSON data to a file err = ioutil.WriteFile(filePath, data, 0644) if err != nil { return fmt.Errorf("failed to write config to file: %v", err) } return nil } // LoadConfig loads the config from the given file path func LoadConfig(filePath string) (*Config, error) { // Check if the file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { return nil, fmt.Errorf("config file does not exist: %s", filePath) } // Read the file content data, err := ioutil.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read config file: %v", err) } // Create a Config object config := &Config{} // Deserialize the JSON data into the Config object err = json.Unmarshal(data, config) if err != nil { return nil, fmt.Errorf("failed to deserialize config: %v", err) } return config, nil } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // api // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const BASE_URL = "https://api.hypixel.net/v2/" const PLAYER_ENDPOINT_URL = "player?" type PlayerResponse struct { Success bool `json:"success"` Cause string `json:"cause,omitempty"` Player Player `json:"player"` } type HypixelApi struct { Key APIKey } func NewAPI(apiKey APIKey) *HypixelApi { return &HypixelApi{ Key: apiKey, } } func (api *HypixelApi) GetPlayer(uuid string) (*Player, error) { _, err := api.canDoRequest() if err != nil { return nil, err } _, err = api.GetPlayerResponse(uuid) if err != nil { return nil, err } return nil, nil } func (api *HypixelApi) GetPlayerResponse(uuid string) (*PlayerResponse, error) { _, err := api.canDoRequest() if err != nil { return nil, err } url := BASE_URL + PLAYER_ENDPOINT_URL + "uuid=" + uuid req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Add("API-Key", raw_key) playerResponse, err := api.doPlayerRequest(req) if !playerResponse.Success { return nil, NewSomeError(playerResponse.Cause, url) } return playerResponse, nil } func (api *HypixelApi) canDoRequest() (bool, error) { return true, nil if api.Key.UsesLeft < 1 { return false, NewAPIKeyRateLimitedException(raw_key, api.Key.UsesLeft, "Key throttle") } return true, nil } func (api *HypixelApi) doPlayerRequest(request *http.Request) (*PlayerResponse, error) { res, err := http.DefaultClient.Do(request) if err != nil { return nil, err } usesLeft, err := strconv.Atoi(res.Header.Get("ratelimit-remaining")) resetStr := res.Header.Get("ratelimit-reset") resetInt, err := strconv.ParseInt(resetStr, 10, 64) if err != nil { log.Fatalf("Error parsing ratelimit-reset: %v", err) } resetTime := time.Unix(resetInt, 0) api.Key.UsesLeft = usesLeft api.Key.ResetDelay = resetTime.Sub(time.Now()) api.Key.TotalUses++ body, err := io.ReadAll(res.Body) if err != nil { return nil, err } defer res.Body.Close() var playerResponse PlayerResponse err = json.Unmarshal(body, &playerResponse) if err != nil { return nil, err } return &playerResponse, nil } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // api // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var raw_key = "" type APIKey struct { TotalUses int UsesLeft int ResetDelay time.Duration } func NewAPIKey(key string) *APIKey { raw_key = key k := &APIKey{TotalUses: 0, UsesLeft: 0, ResetDelay: 0} _, err := k.TestKey() if err != nil { return k } return k } func (api *APIKey) TestKey() (bool, error) { url := BASE_URL + PLAYER_ENDPOINT_URL + "uuid=" + "d245a6e2-349d-405a-b801-48f06d39c9a9" // tqrm req, err := http.NewRequest("GET", url, nil) if err != nil { return false, err } req.Header.Add("API-Key", raw_key) res, err := http.DefaultClient.Do(req) if err != nil { return false, err } usesLeft, err := strconv.Atoi(res.Header.Get("ratelimit-remaining")) resetStr := res.Header.Get("ratelimit-reset") resetInt, err := strconv.ParseInt(resetStr, 10, 64) if err != nil { log.Fatalf("Error parsing ratelimit-reset: %v", err) } resetTime := time.Unix(resetInt, 0) api.UsesLeft = usesLeft api.ResetDelay = resetTime.Sub(time.Now()) api.TotalUses++ body, err := io.ReadAll(res.Body) if err != nil { return false, err } defer res.Body.Close() var playerResponse PlayerResponse err = json.Unmarshal(body, &playerResponse) if err != nil { return false, err } if playerResponse.Success { return true, nil } return false, NewSomeError(url, playerResponse.Cause) } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // player // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - type Player struct { ID string `json:"id"` UUID string `json:"uuid"` PlayerName string `json:"playername"` DisplayName string `json:"displayname"` NetworkExp json.Number `json:"networkExp"` KarmaExp json.Number `json:"karma"` AchievementPoints json.Number `json:"achievementPoints"` UserLanguage string `json:"userLanguage"` RankPlusColor string `json:"rankPlusColor"` Achievements Achievements `json:"achievements"` Stats Stats `json:"stats"` } type Achievements struct { BedwarsLevel int `json:"bedwars_level"` } type Stats struct { Bedwars Bedwars `json:"Bedwars"` } type Bedwars struct { Experience json.Number `json:"Experience"` CurrentWinstreak int `json:"winstreak"` // If this isn't here, they have winstreak disabled (flag) Wins int `json:"wins_bedwars"` Losses int `json:"losses_bedwars"` WLR float64 Kills int `json:"kills_bedwars"` Deaths int `json:"deaths_bedwars"` KDR float64 FinalKills int `json:"final_kills_bedwars"` FinalDeaths int `json:"final_deaths_bedwars"` FKDR float64 BedsBroken int `json:"beds_broken_bedwars"` BedsLost int `json:"beds_lost_bedwars"` BBLR float64 } // TODO: set defaults (kdr, fkdr, bblr, wlr) //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // response_errors // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Errors: // SomeError - Couldn't reach the endpoint (maybe internet issues or blocked from hypxiel? or cloudflare is down, etc) // APIKeyInvalid - Thrown when the key is invalid // APIKeyRateLimited - Thrown when you're being rate limited (reached your call limit) // PlayerNotFoundError - When hypxiel can't find the target player // MalformedUuid - When hypixel returns a malformed UUID error // MalformedPlayer - When hypixel returns a malformed player error // PlayerNotFound - When a target player isn't found (usually by uuid) // TimeOutError - When the api call takes longer than the specified timeout to return // JsonUnmartialingError - When custom unmartialing functions throw errors (NOT a wrapper for generic json unmartial error) // UnknownError - When something that isn't already here happens // SomeError is thrown when the endpoint cannot be reached (e.g., internet issues, blocked from Hypixel, or Cloudflare is down) type SomeError struct { URL string Cause string } // NewSomeError creates a new instance of SomeError func NewSomeError(url, cause string) *SomeError { return &SomeError{ URL: url, Cause: cause, } } // Error returns the error message for SomeError func (e *SomeError) Error() string { return fmt.Sprintf("Unable to reach endpoint: URL: '%s', Cause: '%s'", e.URL, e.Cause) } // IsSomeError checks if the error is a SomeError func IsSomeError(err error) bool { var targetErr *SomeError return errors.As(err, &targetErr) } // APIKeyInvalidException is thrown when the HypixelApi key is invalid type APIKeyInvalidException struct { APIKey string Cause string } // NewAPIKeyInvalidException creates a new instance of APIKeyInvalidException func NewAPIKeyInvalidException(apiKey, cause string) *APIKeyInvalidException { return &APIKeyInvalidException{ APIKey: apiKey, Cause: cause, } } // PlayerNotFoundException is thrown when the HypixelApi cannot find the target player type PlayerNotFoundException struct { URL string PlayerID string Cause string } // NewPlayerNotFoundException helper function to create PlayerNotFoundException error func NewPlayerNotFoundException(url, id, cause string) *PlayerNotFoundException { return &PlayerNotFoundException{ URL: url, PlayerID: id, Cause: cause, } } // Error returns the error message for PlayerNotFoundException func (e *PlayerNotFoundException) Error() string { return fmt.Sprintf("Player not found: Player ID: '%s', fetch cause: '%s', fetched at URL: '%s'", e.PlayerID, e.Cause, e.URL) } // IsPlayerNotFoundException checks if the error is a PlayerNotFoundException error func IsPlayerNotFoundException(err error) bool { var targetErr *PlayerNotFoundException return errors.As(err, &targetErr) } // Error returns the error message for APIKeyInvalidException func (e *APIKeyInvalidException) Error() string { return fmt.Sprintf("HypixelApi key invalid: Key: '%s', Cause: '%s'", e.APIKey, e.Cause) } // IsAPIKeyInvalidException checks if the error is an APIKeyInvalidException func IsAPIKeyInvalidException(err error) bool { var targetErr *APIKeyInvalidException return errors.As(err, &targetErr) } // APIKeyRateLimitedException is thrown when the HypixelApi key has reached its call limit type APIKeyRateLimitedException struct { APIKey string RateLimit int Cause string } // NewAPIKeyRateLimitedException creates a new instance of APIKeyRateLimitedException func NewAPIKeyRateLimitedException(apiKey string, rateLimit int, cause string) *APIKeyRateLimitedException { return &APIKeyRateLimitedException{ APIKey: apiKey, RateLimit: rateLimit, Cause: cause, } } // Error returns the error message for APIKeyRateLimitedException func (e *APIKeyRateLimitedException) Error() string { return fmt.Sprintf("HypixelApi key rate limited: Key: '%s', Rate Limit: '%d', Cause: '%s'", e.APIKey, e.RateLimit, e.Cause) } // IsAPIKeyRateLimitedException checks if the error is an APIKeyRateLimitedException func IsAPIKeyRateLimitedException(err error) bool { var targetErr *APIKeyRateLimitedException return errors.As(err, &targetErr) } // MalformedUUIDException is thrown when Hypixel returns a malformed UUID error type MalformedUUIDException struct { UUID string Cause string } // NewMalformedUUIDException creates a new instance of MalformedUUIDException func NewMalformedUUIDException(uuid, cause string) *MalformedUUIDException { return &MalformedUUIDException{ UUID: uuid, Cause: cause, } } // Error returns the error message for MalformedUUIDException func (e *MalformedUUIDException) Error() string { return fmt.Sprintf("Malformed UUID: UUID: '%s', Cause: '%s'", e.UUID, e.Cause) } // IsMalformedUUIDException checks if the error is a MalformedUUIDException func IsMalformedUUIDException(err error) bool { var targetErr *MalformedUUIDException return errors.As(err, &targetErr) } // MalformedPlayerException is thrown when Hypixel returns a malformed player error type MalformedPlayerException struct { PlayerData string Cause string } // NewMalformedPlayerException creates a new instance of MalformedPlayerException func NewMalformedPlayerException(playerData, cause string) *MalformedPlayerException { return &MalformedPlayerException{ PlayerData: playerData, Cause: cause, } } // Error returns the error message for MalformedPlayerException func (e *MalformedPlayerException) Error() string { return fmt.Sprintf("Malformed player data: Data: '%s', Cause: '%s'", e.PlayerData, e.Cause) } // IsMalformedPlayerException checks if the error is a MalformedPlayerException func IsMalformedPlayerException(err error) bool { var targetErr *MalformedPlayerException return errors.As(err, &targetErr) } // TimeOutException is thrown when the HypixelApi call takes longer than the specified timeout to return type TimeOutException struct { Duration time.Duration URL string Cause string } // NewTimeOutException creates a new instance of TimeOutException func NewTimeOutException(duration time.Duration, url, cause string) *TimeOutException { return &TimeOutException{ Duration: duration, URL: url, Cause: cause, } } // Error returns the error message for TimeOutException func (e *TimeOutException) Error() string { return fmt.Sprintf("Request timed out after '%s' when accessing URL '%s': Cause: '%s'", e.Duration, e.URL, e.Cause) } // IsTimeOutException checks if the error is a TimeOutException func IsTimeOutException(err error) bool { var targetErr *TimeOutException return errors.As(err, &targetErr) } // JSONUnmarshallingException is thrown when custom unmarshalling functions encounter errors type JSONUnmarshallingException struct { Data string Cause string } // NewJSONUnmarshallingException creates a new instance of JSONUnmarshallingException func NewJSONUnmarshallingException(data, cause string) *JSONUnmarshallingException { return &JSONUnmarshallingException{ Data: data, Cause: cause, } } // Error returns the error message for JSONUnmarshallingException func (e *JSONUnmarshallingException) Error() string { return fmt.Sprintf("JSON unmarshalling error: Data: '%s', Cause: '%s'", e.Data, e.Cause) } // IsJSONUnmarshallingException checks if the error is a JSONUnmarshallingException func IsJSONUnmarshallingException(err error) bool { var targetErr *JSONUnmarshallingException return errors.As(err, &targetErr) } // UnknownErrorException is thrown when an unspecified error occurs type UnknownErrorException struct { Cause string } // NewUnknownErrorException creates a new instance of UnknownErrorException func NewUnknownErrorException(cause string) *UnknownErrorException { return &UnknownErrorException{ Cause: cause, } } // Error returns the error message for UnknownErrorException func (e *UnknownErrorException) Error() string { return fmt.Sprintf("Unknown error occurred: Cause: '%s'", e.Cause) } // IsUnknownErrorException checks if the error is an UnknownErrorException func IsUnknownErrorException(err error) bool { var targetErr *UnknownErrorException return errors.As(err, &targetErr) } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // async player fetcher // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // AsyncPlayerFetcher is responsible for fetching Minecraft player data asynchronously type AsyncPlayerFetcher struct { playerName string retries int retryDelay time.Duration timeout time.Duration cache ICache } // NewAsyncPlayerFetcher creates a new AsyncPlayerFetcher with an abstract cache (ICache) func NewAsyncPlayerFetcher(playerName string, cache ICache, retries int, retryDelay time.Duration, timeout time.Duration) *AsyncPlayerFetcher { cache.Init() return &AsyncPlayerFetcher{ playerName: playerName, retries: retries, retryDelay: retryDelay, timeout: timeout, cache: cache, } } // FetchPlayerData fetches the player data asynchronously using channels func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan *FetchedPlayerResult, errorChan chan error) { go func() { cachedData, found := pf.cache.Get(pf.playerName) if found { resultChan <- (*FetchedPlayerResult)(cachedData) return } // If not in cache, make request to Mojang API var player FetchedPlayerResult for i := 0; i < pf.retries; i++ { resp, err := pf.makeRequest(pf.playerName) if err == nil { defer resp.Body.Close() // Decode the response into FetchedPlayerResult if err := json.NewDecoder(resp.Body).Decode(&player); err == nil { // Store the result in the cache and return the data pf.cache.Set(pf.playerName, (*CacheResult)(&player)) pf.cache.Sync() resultChan <- &player return } } time.Sleep(pf.retryDelay) } errorChan <- errors.New("Failed to fetch player data after retries") }() } // makeRequest performs the HTTP request to Mojang API func (pf *AsyncPlayerFetcher) makeRequest(playerName string) (*http.Response, error) { client := http.Client{Timeout: pf.timeout} url := "https://api.mojang.com/users/profiles/minecraft/" + playerName resp, err := client.Get(url) if err != nil || resp.StatusCode != http.StatusOK { return nil, errors.New("Request failed") } return resp, nil } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // cache // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - type CacheResult struct { UUID string `json:"id"` Name string `json:"name"` } type ICache interface { Init() Load() Get(key string) (*CacheResult, bool) Set(key string, data *CacheResult) Save() Sync() Purge() Clear() } // MemoryCache implementation type MemoryCache struct { cache map[string]*CacheResult mu sync.RWMutex } // Init initializes the cache (no-op for MemoryCache) func (c *MemoryCache) Init() { c.cache = make(map[string]*CacheResult) } // Load loads the cache (no-op for MemoryCache) func (c *MemoryCache) Load() {} // Get retrieves an item from the cache func (c *MemoryCache) Get(key string) (*CacheResult, bool) { c.mu.RLock() defer c.mu.RUnlock() value, found := c.cache[key] if !found { return nil, false } return value, true } // Set stores an item in the cache func (c *MemoryCache) Set(key string, data *CacheResult) { c.mu.Lock() defer c.mu.Unlock() c.cache[key] = data } // Save saves the cache (no-op for MemoryCache) func (c *MemoryCache) Save() {} // Sync syncs the cache (no-op for MemoryCache) func (c *MemoryCache) Sync() {} // Purge will be implemented later func (c *MemoryCache) Purge() {} // Clear clears the cache func (c *MemoryCache) Clear() { c.mu.Lock() defer c.mu.Unlock() c.cache = make(map[string]*CacheResult) } // JsonFileCache implementation type JsonFileCache struct { filename string cache map[CacheResult]interface{} mu sync.RWMutex } // Init initializes the cache func (c *JsonFileCache) Init() { c.cache = make(map[CacheResult]interface{}) } // Load loads the cache from a JSON file func (c *JsonFileCache) Load() { c.mu.Lock() defer c.mu.Unlock() file, err := os.Open(c.filename) if err != nil { log.Println("Error opening file:", err) return } defer file.Close() byteValue, err := io.ReadAll(file) if err != nil { log.Println("Error reading file:", err) return } err = json.Unmarshal(byteValue, &c.cache) if err != nil { log.Println("Error unmarshalling JSON:", err) } } // Get retrieves an item from the cache func (c *JsonFileCache) Get(key string) (*CacheResult, bool) { //c.mu.RLock() //defer c.mu.RUnlock() //value, found := c.cache[key] //if !found { // return nil, false //} //return value, true return nil, false } // Set stores an item in the cache func (c *JsonFileCache) Set(key string, data *CacheResult) { //c.mu.Lock() //defer c.mu.Unlock() //c.cache[key] = data } // Save saves the cache to a JSON file func (c *JsonFileCache) Save() { c.mu.Lock() defer c.mu.Unlock() byteValue, err := json.MarshalIndent(c.cache, "", " ") if err != nil { log.Println("Error marshalling JSON:", err) return } err = os.WriteFile(c.filename, byteValue, 0644) if err != nil { log.Println("Error writing file:", err) } } // Sync is the same as Save for the JsonFileCache func (c *JsonFileCache) Sync() { c.Save() } // Purge will be implemented later func (c *JsonFileCache) Purge() {} // Clear clears the cache func (c *JsonFileCache) Clear() { c.mu.Lock() defer c.mu.Unlock() c.cache = make(map[CacheResult]interface{}) } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // player fetcher // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // PlayerFetcher is responsible for fetching Minecraft player data synchronously type PlayerFetcher struct { playerName string retries int retryDelay time.Duration timeout time.Duration cache ICache } type FetchedPlayerResult struct { UUID string `json:"id"` Name string `json:"name"` } // NewPlayerFetcher creates a new PlayerFetcher with an abstract cache (ICache) func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay time.Duration, timeout time.Duration) *PlayerFetcher { cache.Init() return &PlayerFetcher{ playerName: playerName, retries: retries, retryDelay: retryDelay, timeout: timeout, cache: cache, } } // FetchPlayerData fetches the player data synchronously func (pf *PlayerFetcher) FetchPlayerData() (*FetchedPlayerResult, error) { //cachedData, found := pf.cache.Get(pf.playerName) //if found { // return &FetchedPlayerResult{}, nil //} var player FetchedPlayerResult for i := 0; i < pf.retries; i++ { resp, err := pf.makeRequest(pf.playerName) if err == nil { defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(&player); err == nil { // pf.cache.Set(pf.playerName, player) // pf.cache.Sync() return &player, nil } } time.Sleep(pf.retryDelay) } return nil, errors.New("Failed to fetch player data after retries") } // makeRequest performs the HTTP request to Mojang API func (pf *PlayerFetcher) makeRequest(playerName string) (*http.Response, error) { client := http.Client{Timeout: pf.timeout} url := "https://api.mojang.com/users/profiles/minecraft/" + playerName resp, err := client.Get(url) if err != nil || resp.StatusCode != http.StatusOK { return nil, errors.New("Request failed") } return resp, nil } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // tools // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // IsValidUsername checks if the username is valid func IsValidUsername(username string) bool { if len(username) < 3 || len(username) > 16 { return false } validChars := "abcdefghijklmnopqrstuvwxyz1234567890_" for _, char := range strings.ToLower(username) { if !strings.ContainsRune(validChars, char) { return false } } return true } // IsValidUUID checks if a UUID is valid func IsValidUUID(uuid string) bool { matched, _ := regexp.MatchString("^[0-9a-fA-F]{32}$", strings.ReplaceAll(uuid, "-", "")) return matched } // UndashUUID removes dashes from UUIDs func UndashUUID(uuid string) string { return strings.ReplaceAll(uuid, "-", "") } // DashUUID adds dashes to UUIDs at standard positions func DashUUID(uuid string) string { return uuid[:8] + "-" + uuid[8:12] + "-" + uuid[12:16] + "-" + uuid[16:20] + "-" + uuid[20:] }