WIP
This commit is contained in:
parent
b8a6b68888
commit
f489e0c57a
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# IDE Generated Files
|
||||
.vscode/
|
||||
.jb/
|
||||
.cache/
|
||||
.idea/
|
||||
|
||||
# Executables
|
||||
*.exe
|
9
.idea/HypixelStuff.iml
generated
9
.idea/HypixelStuff.iml
generated
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
2
.idea/modules.xml
generated
2
.idea/modules.xml
generated
@ -2,7 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/HypixelStuff.iml" filepath="$PROJECT_DIR$/.idea/HypixelStuff.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/hudly.iml" filepath="$PROJECT_DIR$/.idea/hudly.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
BIN
HypixelStuff.7z
BIN
HypixelStuff.7z
Binary file not shown.
BIN
HypixelStuff.exe
BIN
HypixelStuff.exe
Binary file not shown.
219
app.go
219
app.go
@ -1,219 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
LogPath string
|
||||
API *HypixelAPI
|
||||
UUIDCache *UUIDCache
|
||||
CurrentDisplayTable DisplayTable
|
||||
}
|
||||
|
||||
func NewApp(apiKey string) *App {
|
||||
path := os.Getenv("USERPROFILE")
|
||||
logPath := filepath.Join(path, ".lunarclient", "offline", "multiver", "logs", "latest.log")
|
||||
|
||||
return &App{
|
||||
LogPath: logPath,
|
||||
API: NewHypixelAPI(apiKey),
|
||||
UUIDCache: NewUUIDCache(120 * time.Minute),
|
||||
CurrentDisplayTable: &BedwarsDisplayTable{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Start() {
|
||||
|
||||
rl.SetConfigFlags(rl.FlagWindowTransparent | rl.FlagWindowUndecorated)
|
||||
screenWidth := 800
|
||||
screenHeight := 600
|
||||
rl.InitWindow(int32(screenWidth), int32(screenHeight), "Raylib in Go")
|
||||
defer rl.CloseWindow()
|
||||
|
||||
windowPosition := rl.NewVector2(500, 200)
|
||||
rl.SetWindowPosition(int(int32(windowPosition.X)), int(int32(windowPosition.Y)))
|
||||
|
||||
var panOffset rl.Vector2
|
||||
var dragWindow bool = false
|
||||
|
||||
const LowFps = 8
|
||||
rl.SetTargetFPS(LowFps)
|
||||
|
||||
lineCh := make(chan string)
|
||||
go tailFile(a.LogPath, lineCh)
|
||||
|
||||
ch := make(chan HypixelPlayerResponse)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
sx := 10
|
||||
sy := 10
|
||||
|
||||
for !rl.WindowShouldClose() {
|
||||
mousePosition := rl.GetMousePosition()
|
||||
windowPosition := rl.GetWindowPosition()
|
||||
screenMousePosition := rl.Vector2{
|
||||
X: windowPosition.X + mousePosition.X,
|
||||
Y: windowPosition.Y + mousePosition.Y,
|
||||
}
|
||||
|
||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) && !dragWindow {
|
||||
rl.SetTargetFPS(int32(rl.GetMonitorRefreshRate(0)))
|
||||
dragWindow = true
|
||||
panOffset.X = screenMousePosition.X - float32(windowPosition.X)
|
||||
panOffset.Y = screenMousePosition.Y - float32(windowPosition.Y)
|
||||
}
|
||||
|
||||
if dragWindow {
|
||||
newWindowPositionX := int(screenMousePosition.X - panOffset.X)
|
||||
newWindowPositionY := int(screenMousePosition.Y - panOffset.Y)
|
||||
|
||||
rl.SetWindowPosition(newWindowPositionX, newWindowPositionY)
|
||||
|
||||
if rl.IsMouseButtonReleased(rl.MouseLeftButton) {
|
||||
dragWindow = false
|
||||
rl.SetTargetFPS(LowFps)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case line := <-lineCh:
|
||||
a.onFileEmit(replaceCorruptedRune(line))
|
||||
default:
|
||||
}
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.Color{0, 0, 0, 100})
|
||||
|
||||
yOffset := sy
|
||||
for _, player := range a.CurrentDisplayTable.GetPlayers() {
|
||||
playerName := player.DisplayName
|
||||
bedwarsLevel := player.Achievements.BedwarsStar
|
||||
bedwarsWins := player.Stats.Bedwars.Wins
|
||||
|
||||
rl.DrawText(fmt.Sprintf("Player: %s", playerName), int32(sx), int32(yOffset), 20, rl.Black)
|
||||
yOffset += 25
|
||||
rl.DrawText(fmt.Sprintf("Bedwars Level: %d", bedwarsLevel), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
||||
yOffset += 25
|
||||
rl.DrawText(fmt.Sprintf("Bedwars Wins: %d", bedwarsWins), int32(sx), int32(yOffset), 18, rl.DarkGray)
|
||||
yOffset += 40
|
||||
}
|
||||
rl.EndDrawing()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) onFileEmit(line string) {
|
||||
msg := strings.TrimSpace(line)
|
||||
if len(msg) < 34 {
|
||||
return
|
||||
}
|
||||
submsg := msg[33:]
|
||||
if len(submsg) != 0 {
|
||||
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, ",")
|
||||
for _, player := range players {
|
||||
playerName := strings.TrimSpace(player)
|
||||
playerUUID, err := FetchMCPlayer(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.UUID, playerName, playerName, time.Now()}
|
||||
a.UUIDCache.Add(&cachedPlayer)
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
} else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
|
||||
last, _ := LogBuf.GetSecondToLast()
|
||||
// TODO: Check if moderators
|
||||
if !strings.HasPrefix(last, PartyListMembersPrefix) {
|
||||
return
|
||||
}
|
||||
|
||||
PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
|
||||
for _, player := range strings.Split(PartyMembersMsg, ",") {
|
||||
playerName := strings.TrimSpace(strings.TrimSuffix(player, " ?"))
|
||||
if strings.HasPrefix(playerName, "[") {
|
||||
playerName = strings.Split(playerName, " ")[1]
|
||||
}
|
||||
print("Found Player: '")
|
||||
print(playerName)
|
||||
print("'\n")
|
||||
a.CurrentDisplayTable.AddPlayerN(playerName, a)
|
||||
|
||||
//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 := 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]
|
||||
}
|
||||
print("Found Leader: '")
|
||||
print(playerName)
|
||||
print("'\n")
|
||||
a.CurrentDisplayTable.AddPlayerN(playerName, a)
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
println(submsg)
|
||||
}
|
1
app/app.go
Normal file
1
app/app.go
Normal file
@ -0,0 +1 @@
|
||||
package main
|
69
app/config/config.go
Normal file
69
app/config/config.go
Normal file
@ -0,0 +1,69 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
75
app/main.go
Normal file
75
app/main.go
Normal file
@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hudly/hypixel"
|
||||
"hudly/mcfetch"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var key = "9634ea92-80f0-482f-aebd-b082c6ed6f19"
|
||||
var uuid = "5328930e-d411-49cb-90ad-4e5c7b27dd86"
|
||||
|
||||
func demo() {
|
||||
// Ensure a username is provided as a command-line argument
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatal("Please provide a Minecraft username as a command-line argument.")
|
||||
}
|
||||
|
||||
// Get the username from the command-line arguments
|
||||
username := os.Args[1]
|
||||
|
||||
thing := hypixel.NewAPIKey(key)
|
||||
api := hypixel.NewAPI(*thing)
|
||||
thing.UsesLeft = 11
|
||||
|
||||
// Create a MemoryCache instance
|
||||
memCache := &mcfetch.MemoryCache{}
|
||||
memCache.Init()
|
||||
|
||||
// Create a channel to receive the result
|
||||
resultChan := make(chan map[string]interface{})
|
||||
errorChan := make(chan error)
|
||||
|
||||
// Create an AsyncPlayerFetcher for asynchronous data fetching with MemoryCache
|
||||
asyncFetcher := mcfetch.NewAsyncPlayerFetcher(
|
||||
username, // Minecraft username or UUID
|
||||
memCache, // Pass the memory cache instance
|
||||
2, // Number of retries
|
||||
2*time.Second, // Retry delay
|
||||
5*time.Second, // Request timeout
|
||||
)
|
||||
|
||||
// Start asynchronous data fetching
|
||||
asyncFetcher.FetchPlayerData(resultChan, errorChan)
|
||||
|
||||
// Non-blocking code execution (do something else while waiting)
|
||||
fmt.Println("Fetching data asynchronously...")
|
||||
|
||||
var userID string
|
||||
// Block until we receive data or an error
|
||||
select {
|
||||
case data := <-resultChan:
|
||||
fmt.Printf("Player data: %+v\n", data)
|
||||
|
||||
// Check if "uuid" exists and is not nil
|
||||
if uuid, ok := data["id"].(string); ok {
|
||||
userID = uuid
|
||||
} else {
|
||||
fmt.Println(fmt.Sprintf("%+v", data))
|
||||
log.Fatal("UUID not found or invalid for player")
|
||||
}
|
||||
|
||||
case err := <-errorChan:
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Use the Hypixel API to get additional player data
|
||||
res, err := api.GetPlayerResponse(userID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(fmt.Sprintf("%+v", res))
|
||||
}
|
101
cache.go
101
cache.go
@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CachedUuid struct {
|
||||
CleanUuid string
|
||||
Uuid string
|
||||
PlayerName string
|
||||
TimeFetched time.Time
|
||||
}
|
||||
|
||||
type UUIDCache struct {
|
||||
mu sync.RWMutex
|
||||
lifetimeLimit time.Duration
|
||||
uuidMap map[string]*CachedUuid
|
||||
cleanUuidMap map[string]*CachedUuid
|
||||
playerNameMap map[string]*CachedUuid
|
||||
}
|
||||
|
||||
func NewUUIDCache(lifetime time.Duration) *UUIDCache {
|
||||
return &UUIDCache{
|
||||
lifetimeLimit: lifetime,
|
||||
uuidMap: make(map[string]*CachedUuid),
|
||||
cleanUuidMap: make(map[string]*CachedUuid),
|
||||
playerNameMap: make(map[string]*CachedUuid),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UUIDCache) Add(cachedUuid *CachedUuid) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.uuidMap[cachedUuid.Uuid] = cachedUuid
|
||||
c.cleanUuidMap[cachedUuid.CleanUuid] = cachedUuid
|
||||
c.playerNameMap[cachedUuid.PlayerName] = cachedUuid
|
||||
}
|
||||
|
||||
func (c *UUIDCache) GetByUuid(uuid string) (*CachedUuid, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
val, ok := c.uuidMap[uuid]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (c *UUIDCache) GetByCleanUuid(cleanUuid string) (*CachedUuid, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
val, ok := c.cleanUuidMap[cleanUuid]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (c *UUIDCache) GetByPlayerName(playerName string) (*CachedUuid, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
val, ok := c.playerNameMap[playerName]
|
||||
return val, ok
|
||||
|
||||
}
|
||||
|
||||
func (c *UUIDCache) Get(id string) (*CachedUuid, bool) {
|
||||
val, ok := c.GetByPlayerName(id)
|
||||
if ok {
|
||||
return val, ok
|
||||
}
|
||||
|
||||
val, ok = c.GetByUuid(id)
|
||||
if ok {
|
||||
return val, ok
|
||||
}
|
||||
|
||||
val, ok = c.GetByCleanUuid(id)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (c *UUIDCache) Delete(uuid string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if cachedUuid, ok := c.uuidMap[uuid]; ok {
|
||||
delete(c.uuidMap, cachedUuid.Uuid)
|
||||
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
|
||||
delete(c.playerNameMap, cachedUuid.PlayerName)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UUIDCache) Clean() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for uuid, cachedUuid := range c.uuidMap {
|
||||
if now.Sub(cachedUuid.TimeFetched) > c.lifetimeLimit {
|
||||
delete(c.uuidMap, uuid)
|
||||
delete(c.cleanUuidMap, cachedUuid.CleanUuid)
|
||||
delete(c.playerNameMap, cachedUuid.PlayerName)
|
||||
}
|
||||
}
|
||||
}
|
75
client/client.go
Normal file
75
client/client.go
Normal file
@ -0,0 +1,75 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Connect to the chat server
|
||||
func connectToServer(address string) (net.Conn, error) {
|
||||
conn, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not connect to server: %v", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Read input from the terminal and send it to the server
|
||||
func readInputAndSend(conn net.Conn) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
text, _ := reader.ReadString('\n')
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
// Send the command to the server
|
||||
_, err := conn.Write([]byte(text + "\n"))
|
||||
if err != nil {
|
||||
fmt.Println("Error sending message:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If the user types 'quit', exit the program
|
||||
if text == "quit" {
|
||||
fmt.Println("Goodbye!")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for incoming messages from the server
|
||||
func listenForMessages(conn net.Conn) {
|
||||
reader := bufio.NewReader(conn)
|
||||
for {
|
||||
message, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println("Disconnected from server.")
|
||||
return
|
||||
}
|
||||
fmt.Print(message)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run client.go <server-address>")
|
||||
return
|
||||
}
|
||||
|
||||
serverAddress := os.Args[1]
|
||||
conn, err := connectToServer(serverAddress)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Start a goroutine to listen for incoming messages from the server
|
||||
go listenForMessages(conn)
|
||||
|
||||
// Read input from the terminal and send it to the server
|
||||
readInputAndSend(conn)
|
||||
}
|
66
display.go
66
display.go
@ -1,66 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DisplayTable interface {
|
||||
AddPlayer(player HypixelPlayer)
|
||||
AddPlayerN(player string, app *App)
|
||||
Draw()
|
||||
GetPlayers() []HypixelPlayer
|
||||
}
|
||||
|
||||
type BedwarsDisplayTable struct {
|
||||
players []HypixelPlayer
|
||||
}
|
||||
|
||||
func (b *BedwarsDisplayTable) GetPlayers() []HypixelPlayer {
|
||||
return b.players
|
||||
}
|
||||
|
||||
func (b *BedwarsDisplayTable) AddPlayer(player HypixelPlayer) {
|
||||
b.players = append(b.players, player)
|
||||
}
|
||||
|
||||
func (b *BedwarsDisplayTable) AddPlayerN(player string, app *App) {
|
||||
cachedPlayer, isAlive := app.UUIDCache.Get(player)
|
||||
|
||||
if isAlive {
|
||||
fetchedPlayer, err := app.API.FetchPlayer(cachedPlayer.Uuid)
|
||||
if err != nil {
|
||||
fmt.Println("Error fetching player from Hypixel API:", err)
|
||||
return
|
||||
}
|
||||
b.AddPlayer(fetchedPlayer.Player)
|
||||
} else {
|
||||
playerData, err := FetchMCPlayer(player)
|
||||
if err != nil {
|
||||
fmt.Println("Error fetching player data:", err)
|
||||
return
|
||||
}
|
||||
|
||||
app.UUIDCache.Add(&CachedUuid{
|
||||
CleanUuid: strings.ReplaceAll(playerData.UUID, "-", ""),
|
||||
Uuid: playerData.UUID,
|
||||
PlayerName: playerData.Username,
|
||||
TimeFetched: time.Now(),
|
||||
})
|
||||
|
||||
fetchedPlayer, err := app.API.FetchPlayer(playerData.UUID)
|
||||
if err != nil {
|
||||
fmt.Println("Error fetching player from Hypixel API:", err)
|
||||
return
|
||||
}
|
||||
b.AddPlayer(fetchedPlayer.Player)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BedwarsDisplayTable) Draw() {
|
||||
fmt.Println("=== Bedwars Player Table ===")
|
||||
for _, player := range b.players {
|
||||
fmt.Printf("Player: %s, Score: %d\n", player.DisplayName, player.Achievements.BedwarsStar)
|
||||
}
|
||||
}
|
12
go.mod
12
go.mod
@ -1,11 +1 @@
|
||||
module HypixelStuff
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require github.com/gen2brain/raylib-go/raylib v0.0.0-20241014163942-bf5ef1835077
|
||||
|
||||
require (
|
||||
github.com/ebitengine/purego v0.7.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
)
|
||||
module hudly
|
||||
|
8
go.sum
8
go.sum
@ -1,8 +0,0 @@
|
||||
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
|
||||
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20241014163942-bf5ef1835077 h1:DEJVMa/7rYR4xLwpuSoMGgcaAKXW+64VASWtBWusQNM=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20241014163942-bf5ef1835077/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
217
hypixel.go
217
hypixel.go
@ -1,217 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func errorContext(skip int) string {
|
||||
pc, file, line, ok := runtime.Caller(skip)
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
fn := runtime.FuncForPC(pc).Name()
|
||||
return fmt.Sprintf("%s:%d %s", file, line, fn)
|
||||
}
|
||||
|
||||
func APITraceError(format string, args ...interface{}) error {
|
||||
traceInfo := errorContext(2)
|
||||
return fmt.Errorf("[%s] "+format, append([]interface{}{traceInfo}, args...)...)
|
||||
}
|
||||
|
||||
type HypixelPlayerAchievements struct {
|
||||
BedwarsStar int `json:"bedwars_level"`
|
||||
}
|
||||
|
||||
type HypixelPlayerBedwarsStats struct {
|
||||
Experience int `json:"Experience"`
|
||||
Winstreak int `json:"winstreak"`
|
||||
Wins int `json:"wins_bedwars"`
|
||||
Losses int `json:"losses_bedwars"`
|
||||
GamesPlayed int `json:"games_played_bedwars"`
|
||||
WinLossRatio float32 // wins / gamesplayed
|
||||
|
||||
Kills int `json:"kills_bedwars"`
|
||||
Deaths int `json:"deaths_bedwars"`
|
||||
KillDeathRatio float32 // kills / deaths
|
||||
|
||||
FinalKills int `json:"final_kills_bedwars"`
|
||||
FinalDeaths int `json:"final_deaths_bedwars"`
|
||||
FinalKillDeathRatio float32 // final kills / final deaths
|
||||
|
||||
BedsBroken int `json:"beds_broken_bedwars"`
|
||||
BedsLost int `json:"beds_lost_bedwars"`
|
||||
BedsBrokenLostRatio float32 // beds broken / beds lost
|
||||
}
|
||||
|
||||
func (stats *HypixelPlayerBedwarsStats) CalculateRatios() {
|
||||
// WLR
|
||||
if stats.GamesPlayed > 0 {
|
||||
stats.WinLossRatio = float32(stats.Wins) / float32(stats.GamesPlayed)
|
||||
} else {
|
||||
stats.WinLossRatio = float32(stats.Wins)
|
||||
}
|
||||
|
||||
// KDR
|
||||
if stats.Deaths > 0 {
|
||||
stats.KillDeathRatio = float32(stats.Kills) / float32(stats.Deaths)
|
||||
} else {
|
||||
stats.KillDeathRatio = float32(stats.Kills)
|
||||
}
|
||||
|
||||
// FKDR
|
||||
if stats.FinalDeaths > 0 {
|
||||
stats.FinalKillDeathRatio = float32(stats.FinalKills) / float32(stats.FinalDeaths)
|
||||
} else {
|
||||
stats.FinalKillDeathRatio = float32(stats.FinalKills)
|
||||
}
|
||||
|
||||
// BBLR
|
||||
if stats.BedsLost > 0 {
|
||||
stats.BedsBrokenLostRatio = float32(stats.BedsBroken) / float32(stats.BedsLost)
|
||||
} else {
|
||||
stats.BedsBrokenLostRatio = float32(stats.BedsBroken)
|
||||
}
|
||||
}
|
||||
|
||||
func (stats *HypixelPlayerBedwarsStats) IsWinstreakDisabled() bool {
|
||||
return stats.Winstreak == -1
|
||||
}
|
||||
|
||||
type HypixelPlayerStats struct {
|
||||
Bedwars HypixelPlayerBedwarsStats `json:"Bedwars"`
|
||||
}
|
||||
|
||||
type HypixelPlayer struct {
|
||||
DisplayName string `json:"displayname"`
|
||||
Achievements HypixelPlayerAchievements `json:"achievements"`
|
||||
Stats HypixelPlayerStats `json:"stats"`
|
||||
MonthlyRankColor string `json:"monthlyRankColor"`
|
||||
RankPlusColor string `json:"rankPlusColor"`
|
||||
NetworkExp json.Number `json:"networkExp"`
|
||||
}
|
||||
|
||||
type HypixelPlayerResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Player HypixelPlayer `json:"player"`
|
||||
}
|
||||
|
||||
type HypixelAPIKey struct {
|
||||
Value string
|
||||
Uses int
|
||||
RateLimit time.Duration
|
||||
LimitUsesLeft int
|
||||
}
|
||||
|
||||
func NewHypixelAPIKey(key string) *HypixelAPIKey {
|
||||
return &HypixelAPIKey{
|
||||
Value: key,
|
||||
Uses: 0,
|
||||
RateLimit: time.Millisecond * 1,
|
||||
LimitUsesLeft: -1,
|
||||
}
|
||||
}
|
||||
|
||||
type HypixelAPI struct {
|
||||
BaseURL string
|
||||
APIKey HypixelAPIKey
|
||||
}
|
||||
|
||||
func NewHypixelAPI(apiKey string) *HypixelAPI {
|
||||
// TODO: Ensure valid key before creating
|
||||
key := NewHypixelAPIKey(apiKey)
|
||||
return &HypixelAPI{
|
||||
BaseURL: "https://api.hypixel.net/v2/",
|
||||
APIKey: *key,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HypixelAPI) FetchPlayer(PlayerUUID string) (HypixelPlayerResponse, error) {
|
||||
// TODO: Handle nicked players
|
||||
if len(PlayerUUID) != 32 && len(PlayerUUID) != 36 {
|
||||
return HypixelPlayerResponse{}, fmt.Errorf("invalid Player UUID! please pass in a valid UUID (Not a name!)")
|
||||
}
|
||||
RequestUrl := h.BaseURL + "player?uuid=" + PlayerUUID
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", RequestUrl, nil)
|
||||
if err != nil {
|
||||
return HypixelPlayerResponse{}, APITraceError("failed to create HTTP request: %v", err)
|
||||
}
|
||||
req.Header.Set("API-Key", h.APIKey.Value)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return HypixelPlayerResponse{}, APITraceError("failed to send HTTP request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return HypixelPlayerResponse{}, APITraceError("received non-200 response: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return HypixelPlayerResponse{}, APITraceError("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
var playerResponse HypixelPlayerResponse
|
||||
err = json.Unmarshal(body, &playerResponse)
|
||||
if err != nil {
|
||||
return HypixelPlayerResponse{}, APITraceError("failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
|
||||
if !playerResponse.Success {
|
||||
return HypixelPlayerResponse{}, APITraceError("API returned unsuccessful response")
|
||||
}
|
||||
|
||||
playerResponse.Player.Stats.Bedwars.CalculateRatios()
|
||||
|
||||
return playerResponse, nil
|
||||
}
|
||||
|
||||
func (h *HypixelAPI) FetchPlayerAsync(PlayerUUID string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
|
||||
go func() {
|
||||
result, err := h.FetchPlayer(PlayerUUID)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
resultChan <- result
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *HypixelAPI) FetchPlayersAsync(PlayerUUIDs []string, resultChan chan<- HypixelPlayerResponse, errorChan chan<- error) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, uuid := range PlayerUUIDs {
|
||||
wg.Add(1)
|
||||
|
||||
go func(uuid string) {
|
||||
defer wg.Done()
|
||||
|
||||
result, err := h.FetchPlayer(uuid)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
resultChan <- result
|
||||
}(uuid)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
close(errorChan)
|
||||
}()
|
||||
}
|
106
hypixel/api.go
Normal file
106
hypixel/api.go
Normal file
@ -0,0 +1,106 @@
|
||||
package hypixel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
73
hypixel/key.go
Normal file
73
hypixel/key.go
Normal file
@ -0,0 +1,73 @@
|
||||
package hypixel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
49
hypixel/player.go
Normal file
49
hypixel/player.go
Normal file
@ -0,0 +1,49 @@
|
||||
package hypixel
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
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)
|
248
hypixel/response_errors.go
Normal file
248
hypixel/response_errors.go
Normal file
@ -0,0 +1,248 @@
|
||||
package hypixel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
5
hypixel/tst.go
Normal file
5
hypixel/tst.go
Normal file
@ -0,0 +1,5 @@
|
||||
package hypixel
|
||||
|
||||
func DoThing() int {
|
||||
return 1
|
||||
}
|
49
logger.go
49
logger.go
@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var LogBuf = NewLogBuffer(10)
|
46
main.go
46
main.go
@ -1,46 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const KEY = "5039a811-1b27-4db6-a7f0-c8dd28eeebcd"
|
||||
|
||||
func replaceCorruptedRune(msg string) string {
|
||||
runes := []rune(msg)
|
||||
for i, r := range runes {
|
||||
if r == '<27>' {
|
||||
runes[i] = '§'
|
||||
}
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func 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 main() {
|
||||
app := NewApp(KEY)
|
||||
app.Start()
|
||||
}
|
67
mcfetch/async_player_fetcher.go
Normal file
67
mcfetch/async_player_fetcher.go
Normal file
@ -0,0 +1,67 @@
|
||||
package mcfetch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan map[string]interface{}, errorChan chan error) {
|
||||
go func() {
|
||||
cachedData, found := pf.cache.Get(pf.playerName)
|
||||
if found {
|
||||
resultChan <- cachedData
|
||||
return
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
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(&data); err == nil {
|
||||
pf.cache.Set(pf.playerName, data)
|
||||
pf.cache.Sync()
|
||||
resultChan <- data
|
||||
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
|
||||
}
|
154
mcfetch/cache.go
Normal file
154
mcfetch/cache.go
Normal file
@ -0,0 +1,154 @@
|
||||
package mcfetch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ICache interface {
|
||||
Init()
|
||||
Load()
|
||||
Get(key string) (map[string]interface{}, bool)
|
||||
Set(key string, data map[string]interface{})
|
||||
Save()
|
||||
Sync()
|
||||
Purge()
|
||||
Clear()
|
||||
}
|
||||
|
||||
// MemoryCache implementation
|
||||
type MemoryCache struct {
|
||||
cache map[string]interface{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Init initializes the cache (no-op for MemoryCache)
|
||||
func (c *MemoryCache) Init() {
|
||||
c.cache = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// 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) (map[string]interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
value, found := c.cache[key]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
return value.(map[string]interface{}), true
|
||||
}
|
||||
|
||||
// Set stores an item in the cache
|
||||
func (c *MemoryCache) Set(key string, data map[string]interface{}) {
|
||||
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]interface{})
|
||||
}
|
||||
|
||||
// JsonFileCache implementation
|
||||
type JsonFileCache struct {
|
||||
filename string
|
||||
cache map[string]interface{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Init initializes the cache
|
||||
func (c *JsonFileCache) Init() {
|
||||
c.cache = make(map[string]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) (map[string]interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
value, found := c.cache[key]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
return value.(map[string]interface{}), true
|
||||
}
|
||||
|
||||
// Set stores an item in the cache
|
||||
func (c *JsonFileCache) Set(key string, data map[string]interface{}) {
|
||||
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[string]interface{})
|
||||
}
|
43
mcfetch/notes.md
Normal file
43
mcfetch/notes.md
Normal file
@ -0,0 +1,43 @@
|
||||
## Example Usage:
|
||||
|
||||
```go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"mcfetch"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a channel to receive the result
|
||||
resultChan := make(chan map[string]interface{})
|
||||
errorChan := make(chan error)
|
||||
|
||||
// Create an AsyncPlayerFetcher for asynchronous data fetching
|
||||
asyncFetcher := mcfetch.NewAsyncPlayerFetcher(
|
||||
"Notch", // Minecraft username or UUID
|
||||
3, // Number of retries
|
||||
2*time.Second, // Retry delay
|
||||
5*time.Second, // Request timeout
|
||||
)
|
||||
|
||||
// Start asynchronous data fetching
|
||||
asyncFetcher.FetchPlayerDataAsync(resultChan, errorChan)
|
||||
|
||||
// Non-blocking code execution (do something else while waiting)
|
||||
fmt.Println("Fetching data asynchronously...")
|
||||
|
||||
// Block until we receive data or an error
|
||||
select {
|
||||
case data := <-resultChan:
|
||||
fmt.Printf("Player data: %+v\n", data)
|
||||
case err := <-errorChan:
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
63
mcfetch/player_fetcher.go
Normal file
63
mcfetch/player_fetcher.go
Normal file
@ -0,0 +1,63 @@
|
||||
package mcfetch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PlayerFetcher is responsible for fetching Minecraft player data synchronously
|
||||
type PlayerFetcher struct {
|
||||
playerName string
|
||||
retries int
|
||||
retryDelay time.Duration
|
||||
timeout time.Duration
|
||||
cache ICache
|
||||
}
|
||||
|
||||
// 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() (map[string]interface{}, error) {
|
||||
cachedData, found := pf.cache.Get(pf.playerName)
|
||||
if found {
|
||||
return cachedData, nil
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
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(&data); err == nil {
|
||||
pf.cache.Set(pf.playerName, data)
|
||||
pf.cache.Sync()
|
||||
return data, 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
|
||||
}
|
36
mcfetch/tools.go
Normal file
36
mcfetch/tools.go
Normal file
@ -0,0 +1,36 @@
|
||||
package mcfetch
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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:]
|
||||
}
|
101
mcplayer.go
101
mcplayer.go
@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Property struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type MCPlayer struct {
|
||||
Username string
|
||||
UUID string
|
||||
AvatarURL string
|
||||
SkinTexture string
|
||||
Properties []Property
|
||||
}
|
||||
|
||||
func isValidUUID(id string) bool {
|
||||
r := regexp.MustCompile(`^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
|
||||
return r.MatchString(id)
|
||||
}
|
||||
|
||||
func isValidUsername(name string) bool {
|
||||
r := regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
|
||||
return r.MatchString(name)
|
||||
}
|
||||
|
||||
func fetchFromPlayerDB(id string) (*MCPlayer, error) {
|
||||
url := fmt.Sprintf("https://playerdb.co/api/player/minecraft/%s", id)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result["success"].(bool) == false {
|
||||
return nil, fmt.Errorf("player not found (playerdb): %s", id)
|
||||
}
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
player := data["player"].(map[string]interface{})
|
||||
properties := player["properties"].([]interface{})
|
||||
|
||||
mcPlayer := &MCPlayer{
|
||||
Username: player["username"].(string),
|
||||
UUID: player["id"].(string),
|
||||
AvatarURL: player["avatar"].(string),
|
||||
SkinTexture: player["skin_texture"].(string),
|
||||
}
|
||||
|
||||
for _, p := range properties {
|
||||
prop := p.(map[string]interface{})
|
||||
mcPlayer.Properties = append(mcPlayer.Properties, Property{
|
||||
Name: prop["name"].(string),
|
||||
Value: prop["value"].(string),
|
||||
Signature: prop["signature"].(string),
|
||||
})
|
||||
}
|
||||
|
||||
return mcPlayer, nil
|
||||
}
|
||||
|
||||
func FetchMCPlayer(id string) (*MCPlayer, error) {
|
||||
// TODO: Implement mojang API as the fallback
|
||||
// TODO: Catch Nicks (propagate nick catcher through to HypixelAPI)
|
||||
if isValidUUID(id) {
|
||||
player, err := fetchFromPlayerDB(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return player, nil
|
||||
} else if isValidUsername(id) {
|
||||
player, err := fetchFromPlayerDB(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return player, nil
|
||||
} else {
|
||||
return nil, errors.New("invalid input: not a valid UUID or username")
|
||||
}
|
||||
}
|
40
notes.md
40
notes.md
@ -1,36 +1,6 @@
|
||||
rl.SetWindowState(rl.FlagWindowUndecorated)
|
||||
## Hypixel
|
||||
The key is stored in a KEYFILE (encrypted)
|
||||
|
||||
|
||||
//[07:41:31] [Client thread/INFO]: [CHAT] {"server":"mini208R","gametype":"HOUSING","mode":"dynamic","map":"Base"}
|
||||
|
||||
https://playerdb.co/api/player/minecraft/illyum
|
||||
https://playerdb.co/
|
||||
|
||||
|
||||
[CHAT] ONLINE: angryhacks
|
||||
|
||||
### When to look up stats
|
||||
- on private message / dm
|
||||
- Party Invite
|
||||
- Guild Invite
|
||||
- Friend Invite
|
||||
- When you invite someone to party
|
||||
- When party leader/other member invites party
|
||||
- When party joins (/stream open)
|
||||
- Duel Request (that specific duel's stats)
|
||||
|
||||
### Trackers
|
||||
- Tips?
|
||||
- WDR Reports?
|
||||
|
||||
### Other Stuff To Add
|
||||
If you screenshot a leaderboard, it will check the screenshots folder and try to detect if a screenshot has a leaderboard in it, then it will load the leaderboard in and check for those people's stats and display them off to the side (daily only since other lb are on api)
|
||||
|
||||
|
||||
|
||||
## Building:
|
||||
```bash
|
||||
set CGO_ENABLED=0
|
||||
go build -ldflags="-s -w"
|
||||
upx --best --lzma HypixelStuff.exe
|
||||
```
|
||||
## Proxy Stuff:
|
||||
- Player's Pings
|
||||
- Server TPS
|
BIN
raylib.dll
BIN
raylib.dll
Binary file not shown.
223
server/server.go
Normal file
223
server/server.go
Normal file
@ -0,0 +1,223 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
username string
|
||||
room *Room
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
code string
|
||||
password string
|
||||
clients map[*Client]bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
rooms = make(map[string]*Room)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// Helper function to generate a 4-hexadecimal room code
|
||||
func generateRoomCode() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return fmt.Sprintf("%04x", rand.Intn(0xFFFF))
|
||||
}
|
||||
|
||||
// Handle client connection
|
||||
func handleClient(client *Client) {
|
||||
defer client.conn.Close()
|
||||
|
||||
reader := bufio.NewReader(client.conn)
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if client.room != nil {
|
||||
leaveRoom(client)
|
||||
}
|
||||
fmt.Println("Client disconnected:", client.conn.RemoteAddr())
|
||||
return
|
||||
}
|
||||
processCommand(client, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
|
||||
// Process client commands
|
||||
func processCommand(client *Client, input string) {
|
||||
parts := strings.SplitN(input, " ", 2)
|
||||
if len(parts) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := strings.ToUpper(parts[0])
|
||||
args := ""
|
||||
if len(parts) > 1 {
|
||||
args = parts[1]
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "CREATE":
|
||||
createRoom(client, args)
|
||||
case "JOIN":
|
||||
joinRoom(client, args)
|
||||
case "LEAVE":
|
||||
leaveRoom(client)
|
||||
case "MESSAGE":
|
||||
sendMessage(client, args)
|
||||
case "SYNC":
|
||||
handleSync(client, args)
|
||||
default:
|
||||
client.conn.Write([]byte("Unknown command\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// Create a room with an optional password
|
||||
func createRoom(client *Client, args string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
password := ""
|
||||
if args != "" {
|
||||
password = args // Treat all input after CREATE as a password
|
||||
}
|
||||
|
||||
roomCode := generateRoomCode() // Room code is generated automatically
|
||||
room := &Room{
|
||||
code: roomCode,
|
||||
password: password,
|
||||
clients: make(map[*Client]bool),
|
||||
}
|
||||
|
||||
rooms[roomCode] = room
|
||||
client.conn.Write([]byte(fmt.Sprintf("Room created with code: %s\n", roomCode)))
|
||||
}
|
||||
|
||||
// Join a room using its 4-hexadecimal code and optional password
|
||||
func joinRoom(client *Client, args string) {
|
||||
parts := strings.SplitN(args, " ", 2)
|
||||
roomCode := parts[0]
|
||||
password := ""
|
||||
if len(parts) == 2 {
|
||||
password = parts[1]
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
room, exists := rooms[roomCode]
|
||||
mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
client.conn.Write([]byte("Room not found\n"))
|
||||
return
|
||||
}
|
||||
|
||||
if room.password != "" && room.password != password {
|
||||
client.conn.Write([]byte("Incorrect password\n"))
|
||||
return
|
||||
}
|
||||
|
||||
room.lock.Lock()
|
||||
room.clients[client] = true
|
||||
client.room = room
|
||||
room.lock.Unlock()
|
||||
|
||||
client.conn.Write([]byte(fmt.Sprintf("Joined room: %s\n", roomCode)))
|
||||
broadcastMessage(client.room, fmt.Sprintf("%s has joined the room\n", client.username))
|
||||
}
|
||||
|
||||
// Leave the current room
|
||||
func leaveRoom(client *Client) {
|
||||
if client.room == nil {
|
||||
client.conn.Write([]byte("You are not in any room\n"))
|
||||
return
|
||||
}
|
||||
|
||||
client.room.lock.Lock()
|
||||
delete(client.room.clients, client)
|
||||
client.room.lock.Unlock()
|
||||
|
||||
broadcastMessage(client.room, fmt.Sprintf("%s has left the room\n", client.username))
|
||||
client.conn.Write([]byte("You have left the room\n"))
|
||||
client.room = nil
|
||||
}
|
||||
|
||||
// Send a message to all clients in the current room
|
||||
func sendMessage(client *Client, message string) {
|
||||
if client.room == nil {
|
||||
client.conn.Write([]byte("You are not in any room\n"))
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
formattedMessage := fmt.Sprintf("[%s] %s: %s\n", timestamp, client.username, message)
|
||||
broadcastMessage(client.room, formattedMessage)
|
||||
}
|
||||
|
||||
// Broadcast a message to all clients in the room
|
||||
func broadcastMessage(room *Room, message string) {
|
||||
room.lock.Lock()
|
||||
defer room.lock.Unlock()
|
||||
for client := range room.clients {
|
||||
client.conn.Write([]byte(message))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the SYNC command, expecting a JSON payload
|
||||
func handleSync(client *Client, payload string) {
|
||||
var data map[string]interface{}
|
||||
err := json.Unmarshal([]byte(payload), &data)
|
||||
if err != nil {
|
||||
client.conn.Write([]byte("Invalid JSON\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// You can process the JSON payload here as needed
|
||||
client.conn.Write([]byte("Sync received\n"))
|
||||
}
|
||||
|
||||
// Start the server
|
||||
func startServer() {
|
||||
listener, err := net.Listen("tcp", ":5518")
|
||||
if err != nil {
|
||||
fmt.Println("Error starting server:", err)
|
||||
return
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Println("Server started on port 5518")
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
fmt.Println("Error accepting connection:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go func() {
|
||||
conn.Write([]byte("Enter your username: "))
|
||||
username, _ := bufio.NewReader(conn).ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
client := &Client{
|
||||
conn: conn,
|
||||
username: username,
|
||||
}
|
||||
conn.Write([]byte(fmt.Sprintf("Welcome %s!\n", username)))
|
||||
handleClient(client)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
startServer()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user