From 5ed7778ea3bb020b7a4742bbb176b672acc32274 Mon Sep 17 00:00:00 2001 From: illyum Date: Sat, 26 Oct 2024 21:08:38 -0600 Subject: [PATCH] Add SINGLEFILE --- SINGLEFILE | 1647 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1647 insertions(+) create mode 100644 SINGLEFILE diff --git a/SINGLEFILE b/SINGLEFILE new file mode 100644 index 0000000..37e06e1 --- /dev/null +++ b/SINGLEFILE @@ -0,0 +1,1647 @@ +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:] +}