hudly/SINGLEFILE.go
2024-10-26 21:08:54 -06:00

1648 lines
42 KiB
Go
Raw Permalink Blame History

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 == '<27>' {
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:]
}