Merge pull request 'Merge demo into main branch' (#2) from dev/demo into master

Reviewed-on: http://localhost:3001/illyum/hudly/pulls/2
This commit is contained in:
illyum 2024-10-30 03:43:53 -06:00
commit bc6c2c5988
15 changed files with 2944 additions and 335 deletions

76
README.md Normal file
View File

@ -0,0 +1,76 @@
//[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:
#### Windows
```bash
set CGO_ENABLED=0
go build -ldflags="-s -w"
upx --best --lzma hudly.exe
```
#### Mac (intel x86_64)
```bash
set GOOS=darwin
set GOARCH=amd64
set CGO_ENABLED=0
go build -ldflags="-s -w"
upx --best --lzma --force-macos hudly
```
NOTE: macOS is currently not supported
#### Mac (apple silicon ARM)
```bash
set GOOS=darwin
set GOARCH=arm64
set CGO_ENABLED=0
go build -ldflags="-s -w"
upx --best --lzma --force-macos hudly
```
NOTE: macOS is currently not supported
# TODOS/Limitation
- (api_key) Incorrect structure (api headers don't exist if key is invalid) so you get the wrong error code
- (build) Requires google's UUID library (too big for my liking)
- (client) No keep-alive implemented
- (client) No room closure detection
- (client) You can't see and send data (sender needs to have 2 clients, 1 to host and 1 to read)
- (client/config) Hard coded ip address / port
- (config) No Config (hard code key)
- (demo) Only in-memory uuid cache
- (demo) Lunar Client ONLY (default log location only)
- (demo) Requires working key to function
- (demo) Windows client sender ONLY (not correct log path locator)
- (demo) does NOT show nicked players (doesn't crash)
- (gui) Just terminal for now
- (hypixel_api) No cache
- (player) Only bedwars stats
- (server) Terrible status messages
- (server/server-config) Hard coded port
- (uuid cache) no lifetime (probably isn't needed but still)

1647
SINGLEFILE.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
package main

8
app/demo.go Normal file
View File

@ -0,0 +1,8 @@
package main
var key = "f6999283-43ba-413e-a04d-32dbde98f423"
func main() {
var demoApp = NewDemoApp(key)
demoApp.Start()
}

50
app/logbuf.go Normal file
View File

@ -0,0 +1,50 @@
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)

60
app/logger.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"fmt"
"log"
"os"
"runtime"
"time"
)
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")
}

View File

@ -1,75 +1,404 @@
package main package main
import ( import (
"bufio"
"encoding/json"
"fmt" "fmt"
"github.com/google/uuid"
netclient "hudly/client"
"hudly/hypixel" "hudly/hypixel"
"hudly/mcfetch" "hudly/mcfetch"
"log" "log"
"os" "os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time" "time"
) )
var key = "9634ea92-80f0-482f-aebd-b082c6ed6f19" type PlayerWrapper struct {
var uuid = "5328930e-d411-49cb-90ad-4e5c7b27dd86" Player hypixel.Player `json:"player"`
}
func demo() { func replaceCorruptedRune(msg string) string {
// Ensure a username is provided as a command-line argument runes := []rune(msg)
if len(os.Args) < 2 { for i, r := range runes {
log.Fatal("Please provide a Minecraft username as a command-line argument.") 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 *netclient.Client
API *hypixel.HypixelApi
MemCache *mcfetch.MemoryCache
LogBuf *LogBuffer
PartyBuilder []map[string]interface{}
}
func NewDemoApp(key string) *DemoApp {
var api_key = hypixel.NewAPIKey(key)
app := &DemoApp{
API: hypixel.NewAPI(*api_key),
MemCache: &mcfetch.MemoryCache{},
LogBuf: NewLogBuffer(10),
PartyBuilder: []map[string]interface{}{},
}
app.MemCache.Init()
return app
}
func (app *DemoApp) FetchMCPlayer(name string) (*mcfetch.FetchedPlayerResult, error) {
asyncFetcher := mcfetch.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)
} }
// Get the username from the command-line arguments OnlinePrefix := "[CHAT] ONLINE: "
username := os.Args[1] PartyListSeparatorLinePrefix := "[CHAT] -----------------------------------------------------"
//PartyMemberCountPrefix := "[CHAT] Party Members ("
PartyLeaderPrefix := "[CHAT] Party Leader: "
PartyListMembersPrefix := "[CHAT] Party Members: "
thing := hypixel.NewAPIKey(key) if strings.HasPrefix(submsg, OnlinePrefix) { // Online Message
api := hypixel.NewAPI(*thing) newsubmsg := strings.TrimPrefix(submsg, OnlinePrefix)
thing.UsesLeft = 11 players := strings.Split(newsubmsg, ",")
var online []mcfetch.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 := mcfetch.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)
// Create a MemoryCache instance } else if strings.HasPrefix(submsg, PartyListSeparatorLinePrefix) { // Party List
memCache := &mcfetch.MemoryCache{} last, _ := app.LogBuf.GetSecondToLast()
memCache.Init() // TODO: Check if moderators
if !strings.HasPrefix(last, PartyListMembersPrefix) {
// Create a channel to receive the result return
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: PartyMembersMsg := strings.TrimPrefix(last, PartyListMembersPrefix)
log.Fatal(err) var ppl []mcfetch.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 := mcfetch.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 := mcfetch.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
} }
// Use the Hypixel API to get additional player data println(submsg)
res, err := api.GetPlayerResponse(userID) }
if err != nil {
panic(err) func (app *DemoApp) tailFile(path string, lineCh chan<- string) {
} file, err := os.Open(path)
fmt.Println(fmt.Sprintf("%+v", res)) 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 []mcfetch.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 = netclient.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 []*hypixel.Player
for _, wrapper := range playerWrappers {
players = append(players, &wrapper.Player)
}
app.DisplayPlayers(players)
}
func (app *DemoApp) DisplayPlayers(players []*hypixel.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("|----------------------|------------|------------|")
} }

View File

@ -1,75 +1,239 @@
package client package client
import ( import (
"bufio" "encoding/binary"
"encoding/json"
"errors"
"fmt" "fmt"
"io"
"net" "net"
"os"
"strings"
) )
// Connect to the chat server const PORT = ":5518"
func connectToServer(address string) (net.Conn, error) {
conn, err := net.Dial("tcp", address) 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 { if err != nil {
return nil, fmt.Errorf("could not connect to server: %v", err) return nil, fmt.Errorf("failed to connect to server: %w", err)
} }
return conn, nil return &Client{
Conn: conn,
ClientID: clientID,
DataChannel: make(chan string),
}, nil
} }
// Read input from the terminal and send it to the server // CreateRoom creates a new room on the server.
func readInputAndSend(conn net.Conn) { func (c *Client) CreateRoom(password string) (string, error) {
reader := bufio.NewReader(os.Stdin) request := CreateRoomRequestPacket{
for { UserID: c.ClientID,
fmt.Print("> ") Password: password,
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
}
} }
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
} }
// Listen for incoming messages from the server // JoinRoom joins an existing room on the server.
func listenForMessages(conn net.Conn) { func (c *Client) JoinRoom(roomCode, password string) error {
reader := bufio.NewReader(conn) request := JoinRequestPacket{
for { UserID: c.ClientID,
message, err := reader.ReadString('\n') RoomCode: roomCode,
if err != nil { Password: password,
fmt.Println("Disconnected from server.")
return
}
fmt.Print(message)
} }
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 main() { func (c *Client) ListenForData() {
if len(os.Args) < 2 { go func() {
fmt.Println("Usage: go run client.go <server-address>") for {
return 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,
} }
serverAddress := os.Args[1] if err := c.sendPacket(SEND_DATA_REQUEST, request); err != nil {
conn, err := connectToServer(serverAddress) 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 { if err != nil {
fmt.Println(err) return fmt.Errorf("failed to encode packet: %w", err)
return
} }
defer conn.Close() return writePacket(c.Conn, byte(packetID), packetData)
}
// Start a goroutine to listen for incoming messages from the server
go listenForMessages(conn) // receivePacket reads a response from the server for a given packet ID.
func (c *Client) receivePacket(expected PacketID, v interface{}) error {
// Read input from the terminal and send it to the server packetID, data, err := readPacket(c.Conn)
readInputAndSend(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
} }

View File

@ -1,6 +1,9 @@
package config package config
import ( import (
"encoding/json"
"fmt"
"io/ioutil"
"os" "os"
"path" "path"
"runtime" "runtime"
@ -67,3 +70,45 @@ func GetDefaultConfig() *Config {
}, },
} }
} }
// 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
}

7
go.mod
View File

@ -1 +1,8 @@
module hudly module hudly
go 1.23.0
require (
github.com/google/uuid v1.6.0
)

View File

@ -1,5 +0,0 @@
package hypixel
func DoThing() int {
return 1
}

View File

@ -28,24 +28,27 @@ func NewAsyncPlayerFetcher(playerName string, cache ICache, retries int, retryDe
} }
} }
// FetchPlayerData fetches the player data asynchronously // FetchPlayerData fetches the player data asynchronously using channels
func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan map[string]interface{}, errorChan chan error) { func (pf *AsyncPlayerFetcher) FetchPlayerData(resultChan chan *FetchedPlayerResult, errorChan chan error) {
go func() { go func() {
cachedData, found := pf.cache.Get(pf.playerName) cachedData, found := pf.cache.Get(pf.playerName)
if found { if found {
resultChan <- cachedData resultChan <- (*FetchedPlayerResult)(cachedData)
return return
} }
var data map[string]interface{} // If not in cache, make request to Mojang API
var player FetchedPlayerResult
for i := 0; i < pf.retries; i++ { for i := 0; i < pf.retries; i++ {
resp, err := pf.makeRequest(pf.playerName) resp, err := pf.makeRequest(pf.playerName)
if err == nil { if err == nil {
defer resp.Body.Close() defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&data); err == nil { // Decode the response into FetchedPlayerResult
pf.cache.Set(pf.playerName, data) 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() pf.cache.Sync()
resultChan <- data resultChan <- &player
return return
} }
} }

View File

@ -8,11 +8,16 @@ import (
"sync" "sync"
) )
type CacheResult struct {
UUID string `json:"id"`
Name string `json:"name"`
}
type ICache interface { type ICache interface {
Init() Init()
Load() Load()
Get(key string) (map[string]interface{}, bool) Get(key string) (*CacheResult, bool)
Set(key string, data map[string]interface{}) Set(key string, data *CacheResult)
Save() Save()
Sync() Sync()
Purge() Purge()
@ -21,31 +26,31 @@ type ICache interface {
// MemoryCache implementation // MemoryCache implementation
type MemoryCache struct { type MemoryCache struct {
cache map[string]interface{} cache map[string]*CacheResult
mu sync.RWMutex mu sync.RWMutex
} }
// Init initializes the cache (no-op for MemoryCache) // Init initializes the cache (no-op for MemoryCache)
func (c *MemoryCache) Init() { func (c *MemoryCache) Init() {
c.cache = make(map[string]interface{}) c.cache = make(map[string]*CacheResult)
} }
// Load loads the cache (no-op for MemoryCache) // Load loads the cache (no-op for MemoryCache)
func (c *MemoryCache) Load() {} func (c *MemoryCache) Load() {}
// Get retrieves an item from the cache // Get retrieves an item from the cache
func (c *MemoryCache) Get(key string) (map[string]interface{}, bool) { func (c *MemoryCache) Get(key string) (*CacheResult, bool) {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
value, found := c.cache[key] value, found := c.cache[key]
if !found { if !found {
return nil, false return nil, false
} }
return value.(map[string]interface{}), true return value, true
} }
// Set stores an item in the cache // Set stores an item in the cache
func (c *MemoryCache) Set(key string, data map[string]interface{}) { func (c *MemoryCache) Set(key string, data *CacheResult) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.cache[key] = data c.cache[key] = data
@ -64,19 +69,19 @@ func (c *MemoryCache) Purge() {}
func (c *MemoryCache) Clear() { func (c *MemoryCache) Clear() {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.cache = make(map[string]interface{}) c.cache = make(map[string]*CacheResult)
} }
// JsonFileCache implementation // JsonFileCache implementation
type JsonFileCache struct { type JsonFileCache struct {
filename string filename string
cache map[string]interface{} cache map[CacheResult]interface{}
mu sync.RWMutex mu sync.RWMutex
} }
// Init initializes the cache // Init initializes the cache
func (c *JsonFileCache) Init() { func (c *JsonFileCache) Init() {
c.cache = make(map[string]interface{}) c.cache = make(map[CacheResult]interface{})
} }
// Load loads the cache from a JSON file // Load loads the cache from a JSON file
@ -104,21 +109,22 @@ func (c *JsonFileCache) Load() {
} }
// Get retrieves an item from the cache // Get retrieves an item from the cache
func (c *JsonFileCache) Get(key string) (map[string]interface{}, bool) { func (c *JsonFileCache) Get(key string) (*CacheResult, bool) {
c.mu.RLock() //c.mu.RLock()
defer c.mu.RUnlock() //defer c.mu.RUnlock()
value, found := c.cache[key] //value, found := c.cache[key]
if !found { //if !found {
return nil, false // return nil, false
} //}
return value.(map[string]interface{}), true //return value, true
return nil, false
} }
// Set stores an item in the cache // Set stores an item in the cache
func (c *JsonFileCache) Set(key string, data map[string]interface{}) { func (c *JsonFileCache) Set(key string, data *CacheResult) {
c.mu.Lock() //c.mu.Lock()
defer c.mu.Unlock() //defer c.mu.Unlock()
c.cache[key] = data //c.cache[key] = data
} }
// Save saves the cache to a JSON file // Save saves the cache to a JSON file
@ -150,5 +156,5 @@ func (c *JsonFileCache) Purge() {}
func (c *JsonFileCache) Clear() { func (c *JsonFileCache) Clear() {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.cache = make(map[string]interface{}) c.cache = make(map[CacheResult]interface{})
} }

View File

@ -16,6 +16,11 @@ type PlayerFetcher struct {
cache ICache cache ICache
} }
type FetchedPlayerResult struct {
UUID string `json:"id"`
Name string `json:"name"`
}
// NewPlayerFetcher creates a new PlayerFetcher with an abstract 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 { func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay time.Duration, timeout time.Duration) *PlayerFetcher {
cache.Init() cache.Init()
@ -29,21 +34,21 @@ func NewPlayerFetcher(playerName string, cache ICache, retries int, retryDelay t
} }
// FetchPlayerData fetches the player data synchronously // FetchPlayerData fetches the player data synchronously
func (pf *PlayerFetcher) FetchPlayerData() (map[string]interface{}, error) { func (pf *PlayerFetcher) FetchPlayerData() (*FetchedPlayerResult, error) {
cachedData, found := pf.cache.Get(pf.playerName) //cachedData, found := pf.cache.Get(pf.playerName)
if found { //if found {
return cachedData, nil // return &FetchedPlayerResult{}, nil
} //}
var data map[string]interface{} var player FetchedPlayerResult
for i := 0; i < pf.retries; i++ { for i := 0; i < pf.retries; i++ {
resp, err := pf.makeRequest(pf.playerName) resp, err := pf.makeRequest(pf.playerName)
if err == nil { if err == nil {
defer resp.Body.Close() defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&data); err == nil { if err := json.NewDecoder(resp.Body).Decode(&player); err == nil {
pf.cache.Set(pf.playerName, data) // pf.cache.Set(pf.playerName, player)
pf.cache.Sync() // pf.cache.Sync()
return data, nil return &player, nil
} }
} }
time.Sleep(pf.retryDelay) time.Sleep(pf.retryDelay)

View File

@ -1,201 +1,99 @@
package main package main
import ( import (
"bufio" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log"
"math/rand" "math/rand"
"net" "net"
"strings"
"sync" "sync"
"time" "time"
) )
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
)
type Client struct { type Client struct {
conn net.Conn Conn net.Conn
username string ClientID string
room *Room RoomCode string
} }
type Room struct { type Room struct {
code string Code string
password string Password string
clients map[*Client]bool Clients []*Client
lock sync.Mutex
} }
var ( var rooms = map[string]*Room{}
rooms = make(map[string]*Room) var clients = map[string]*Client{}
mu sync.Mutex var mu sync.Mutex
)
// Helper function to generate a 4-hexadecimal room code func readPacket(conn net.Conn) (PacketID, []byte, error) {
func generateRoomCode() string { // First, read the length of the packet (4 bytes)
rand.Seed(time.Now().UnixNano()) var length uint32
return fmt.Sprintf("%04x", rand.Intn(0xFFFF)) if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return 0, nil, err
}
// Then read the packet ID (1 byte)
var messageType byte
if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil {
return 0, nil, err
}
// Read the remaining data (length - 5 bytes, since 4 bytes for length and 1 byte for messageType)
data := make([]byte, length-5)
if _, err := io.ReadFull(conn, data); err != nil {
return 0, nil, err
}
return PacketID(int(messageType)), data, nil
} }
// Handle client connection func writePacket(conn net.Conn, messageType byte, data []byte) error {
func handleClient(client *Client) { // Calculate the total length of the packet
defer client.conn.Close() // 4 bytes for length, 1 byte for messageType
length := uint32(5 + len(data))
reader := bufio.NewReader(client.conn) // Write the length and the message type
if err := binary.Write(conn, binary.BigEndian, length); err != nil {
for { return err
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))
} }
if err := binary.Write(conn, binary.BigEndian, messageType); err != nil {
return err
}
// Write the data
_, err := conn.Write(data)
return err
} }
// Process client commands func main() {
func processCommand(client *Client, input string) { listener, err := net.Listen("tcp", PORT)
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 { if err != nil {
fmt.Println("Error starting server:", err) fmt.Println("Error starting server:", err)
return return
} }
defer listener.Close() defer listener.Close()
fmt.Println("Server started on port 5518") fmt.Println("Server started on port", PORT)
for { for {
conn, err := listener.Accept() conn, err := listener.Accept()
@ -203,21 +101,338 @@ func startServer() {
fmt.Println("Error accepting connection:", err) fmt.Println("Error accepting connection:", err)
continue continue
} }
go handleClient(conn)
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() { type ConnectionRequestPacket struct {
startServer() UserID string
RoomCode string
Password string
}
type ConnectionResponsePacket struct {
Success bool
Reason string
}
type CreateRoomRequestPacket struct {
UserID string
Password string
}
type CreateRoomResponsePacket struct {
Success bool
Reason string
RoomCode string
}
type JoinRequestPacket struct {
UserID string
Password string
RoomCode string
}
type JoinRequestResponsePacket struct {
Success bool
Reason string
CurrentData string
}
type SendDataRequestPacket struct {
UserID string
Data string
}
type ForwardDataPacket struct {
Data string
}
type SendDataResponsePacket struct {
Success bool
Reason string
}
func handleClient(conn net.Conn) {
// defer conn.Close()
for {
var packetId PacketID
packetId, data, err := readPacket(conn)
if err != nil {
fmt.Println("Error reading packet (handleclient) :", err)
handleLeaveRoom(conn)
return
}
switch packetId {
case CONNECT_REQUEST:
var packet ConnectionRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleConnectRequest(conn, packet)
case CREATE_REQUEST:
var packet CreateRoomRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleCreateRoomRequest(conn, packet)
case JOIN_REQUEST:
var packet JoinRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleJoinRequest(conn, packet)
case SEND_DATA_REQUEST:
var packet SendDataRequestPacket
if err := json.Unmarshal(data, &packet); err != nil {
fmt.Println("Error decoding connection request:", err)
return
}
handleSendData(conn, packet)
case LEAVE_ROOM:
handleLeaveRoom(conn)
}
}
}
func handleConnectRequest(conn net.Conn, packet ConnectionRequestPacket) {
fmt.Println("Incoming connection request")
room := findRoom(packet.RoomCode)
if room == nil {
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: false,
Reason: "room does not exist",
}))
fmt.Println("Attempted room does not exist")
return
}
if room.Password == "" || packet.Password == room.Password {
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: true,
Reason: "",
}))
fmt.Println("Invalid password")
return
}
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: false,
Reason: "invalid password",
}))
fmt.Println("Connected user to room ")
}
func handleCreateRoomRequest(conn net.Conn, packet CreateRoomRequestPacket) {
if !isValidPassword(packet.Password) {
writePacket(conn, byte(CREATE_RESPONSE), encodeResponsePacket(CreateRoomResponsePacket{
Success: false,
Reason: "invalid password",
RoomCode: "",
}))
return
}
code := generateRandomRoomCode()
room := &Room{
Code: code,
Password: packet.Password,
Clients: []*Client{},
}
mu.Lock()
rooms[code] = room
mu.Unlock()
writePacket(conn, byte(CREATE_RESPONSE), encodeResponsePacket(CreateRoomResponsePacket{
Success: true,
Reason: "",
RoomCode: code,
}))
}
func handleJoinRequest(conn net.Conn, packet JoinRequestPacket) {
room := findRoom(packet.RoomCode)
if room == nil {
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: false,
Reason: "room not found",
}))
return
}
if !isValidPassword(packet.Password) {
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: false,
Reason: "invalid password",
}))
return
}
if room.Password == "" || room.Password == packet.Password {
client := &Client{
Conn: conn,
ClientID: packet.UserID,
RoomCode: packet.RoomCode,
}
mu.Lock()
room.Clients = append(room.Clients, client)
clients[packet.UserID] = client
mu.Unlock()
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: true,
Reason: "",
CurrentData: "{}",
// TODO: Send current data
}))
return
}
writePacket(conn, byte(JOIN_RESPONSE), encodeResponsePacket(JoinRequestResponsePacket{
Success: false,
Reason: "invalid password",
}))
return
}
func handleSendData(conn net.Conn, packet SendDataRequestPacket) {
fmt.Println("Incoming send data request")
mu.Lock()
client, exists := clients[packet.UserID]
mu.Unlock()
if !exists || client.RoomCode == "" {
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
Success: false,
Reason: "client not in a room",
}))
return
}
room := findRoom(client.RoomCode)
if room == nil {
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
Success: false,
Reason: "room not found",
}))
return
}
dataPacket := encodeResponsePacket(ForwardDataPacket{
Data: packet.Data,
})
mu.Lock()
for _, roomClient := range room.Clients {
// if roomClient.ClientID != packet.UserID {
// // Send data to all other clients except the sender
// writePacket(roomClient.Conn, byte(READ_DATA), dataPacket)
// }
writePacket(roomClient.Conn, byte(READ_DATA), dataPacket)
}
mu.Unlock()
writePacket(conn, byte(SEND_DATA_RESPONSE), encodeResponsePacket(SendDataResponsePacket{
Success: true,
Reason: "",
}))
}
func handleLeaveRoom(conn net.Conn) {
mu.Lock()
defer mu.Unlock()
var leavingClient *Client
for _, client := range clients {
if client.Conn == conn {
leavingClient = client
break
}
}
if leavingClient == nil {
fmt.Println("Client not found")
return
}
room := rooms[leavingClient.RoomCode]
if room == nil {
fmt.Println("Room not found")
return
}
for i, client := range room.Clients {
if client == leavingClient {
room.Clients = append(room.Clients[:i], room.Clients[i+1:]...)
break
}
}
// Delete the room if its empty
if len(room.Clients) == 0 {
delete(rooms, room.Code)
fmt.Println("Room", room.Code, "deleted")
}
delete(clients, leavingClient.ClientID)
done := make(chan struct{})
go func() {
leavingClient.Conn.Close()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
log.Fatalln("Timeout while closing leaving client's connection")
}
fmt.Println("Client", leavingClient.ClientID, "left the room and connection closed")
}
func findRoom(code string) *Room {
mu.Lock()
defer mu.Unlock()
return rooms[code]
}
func encodeResponsePacket(packet interface{}) []byte {
data, err := json.Marshal(packet)
if err != nil {
fmt.Println("Error encoding response packet:", err)
return nil
}
return data
}
func generateRandomRoomCode() string {
validChars := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
code := make([]byte, 4)
for i := range code {
code[i] = validChars[rand.Intn(len(validChars))]
}
return string(code)
}
func isValidPassword(password string) bool {
validChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!()[]{}<>-_"
charMap := make(map[rune]bool)
for _, char := range validChars {
charMap[char] = true
}
for _, char := range password {
if !charMap[char] {
return false
}
}
return true
} }