1648 lines
42 KiB
Go
1648 lines
42 KiB
Go
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:]
|
||
}
|