Compare commits

...

2 Commits

Author SHA1 Message Date
65caea328b wip: add server and client logic 2024-10-25 22:20:55 -06:00
c24ae92a81 Add README.md 2024-10-22 18:52:58 -06:00
7 changed files with 976 additions and 238 deletions

33
README.md Normal file
View File

@ -0,0 +1,33 @@
//[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
```

View File

@ -1 +1,107 @@
package main
import (
"github.com/haleyrom/skia-go"
"image/color"
"log"
"runtime"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/glfw/v3.3/glfw"
)
func init() {
// GLFW event handling must run on the main OS thread
runtime.LockOSThread()
}
func main() {
// Initialize GLFW
if err := glfw.Init(); err != nil {
log.Fatalf("failed to initialize GLFW: %v", err)
}
defer glfw.Terminate()
// Create GLFW window
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
window, err := glfw.CreateWindow(800, 600, "GLFW + Skia Demo", nil, nil)
if err != nil {
log.Fatalf("failed to create GLFW window: %v", err)
}
window.MakeContextCurrent()
// Initialize OpenGL
if err := gl.Init(); err != nil {
log.Fatalf("failed to initialize OpenGL: %v", err)
}
// Set up Skia surface
glInterface := skia.NewNativeGrGlinterface()
if glInterface == nil {
log.Fatalf("failed to create Skia OpenGL interface")
}
grContext := skia.NewGLGrContext(glInterface)
if grContext == nil {
log.Fatalf("failed to create Skia GrContext")
}
// Get framebuffer info
var framebufferInfo skia.GrGlFramebufferinfo
var fboID int32
gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &fboID)
framebufferInfo.FFBOID = uint32(fboID)
framebufferInfo.FFormat = gl.RGBA8
width, height := window.GetSize()
renderTarget := skia.NewGlGrBackendrendertarget(int32(width), int32(height), 1, 8, &framebufferInfo)
if renderTarget == nil {
log.Fatalf("failed to create Skia render target")
}
surface := skia.NewSurfaceBackendRenderTarget(grContext, renderTarget, skia.GR_SURFACE_ORIGIN_BOTTOM_LEFT, skia.SK_COLORTYPE_RGBA_8888, nil, nil)
if surface == nil {
log.Fatalf("failed to create Skia surface")
}
defer surface.Unref()
c := color.RGBA{R: 255, G: 100, B: 50, A: 255}
f := skia.NewColor4f(c)
colorWhite := f.ToColor()
for !window.ShouldClose() {
gl.ClearColor(0.3, 0.3, 0.3, 1.0)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
canvas := surface.GetCanvas()
paint := skia.NewPaint()
paint.SetColor(0xffff0000)
// Clear the canvas with white color
canvas.Clear(colorWhite)
// Create a rectangle and draw it
rect := skia.Rect{
Left: 100,
Top: 100,
Right: 300,
Bottom: 300,
}
canvas.DrawRect(&rect, paint)
grContext.Flush()
// Swap OpenGL buffers
window.SwapBuffers()
// Poll for GLFW events
glfw.PollEvents()
}
// Cleanup
surface.Unref()
grContext.Unref()
}

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 main() {
logger := NewCustomLogger()
logger.Info("This is an info message")
logger.Error("This is an error message")
}

View File

@ -1,7 +1,10 @@
package main
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
netclient "hudly/client"
"hudly/hypixel"
"hudly/mcfetch"
"log"
@ -9,8 +12,8 @@ import (
"time"
)
var key = "9634ea92-80f0-482f-aebd-b082c6ed6f19"
var uuid = "5328930e-d411-49cb-90ad-4e5c7b27dd86"
var key = "ccebff0f-939a-4afe-b5b3-30a7a665ee38"
var UUID = "5328930e-d411-49cb-90ad-4e5c7b27dd86"
func demo() {
// Ensure a username is provided as a command-line argument
@ -73,3 +76,157 @@ func demo() {
}
fmt.Println(fmt.Sprintf("%+v", res))
}
func main() {
var cmd string
fmt.Print(" | CREATE\n | JOIN\nEnter Choice:\n>")
fmt.Scanln(&cmd)
if cmd != "CREATE" && cmd != "JOIN" {
fmt.Println("Invalid command.")
return
}
client, err := netclient.NewClient("0.0.0.0", uuid.New().String())
if err != nil {
log.Fatalf("Failed to create client: %v", err)
return
}
var code string
if cmd == "CREATE" {
var err error
code, err = client.CreateRoom("")
if err != nil {
log.Fatal(err)
return
}
fmt.Println("Created room:", code)
err = client.JoinRoom(code, "password")
if err != nil {
log.Fatal(err)
return
}
println("Connected to room")
} else if cmd == "JOIN" {
var password string
fmt.Print("Enter Room Code:\n>")
fmt.Scanln(&code)
fmt.Print("Enter Room Password:\n>")
fmt.Scanln(&password)
err := client.JoinRoom(code, password)
if err != nil {
log.Fatal(err)
return
}
fmt.Println("Joined room:", code)
}
client.ListenForData()
// Handle received data in a separate goroutine
go func() {
for data := range client.DataChannel {
fmt.Printf("Received data: %s\n", data)
}
}()
if cmd == "CREATE" {
println("Sleeping...")
time.Sleep(time.Second * 20)
println("Continuing!")
thing := hypixel.NewAPIKey(key)
api := hypixel.NewAPI(*thing)
memCache := &mcfetch.MemoryCache{}
memCache.Init()
resultChan := make(chan map[string]interface{})
errorChan := make(chan error)
players := []string{"illyum", "ergopunch"}
stuff := []map[string]interface{}{} // List to hold player JSON objects
println("Getting UUIDs")
for _, player := range players {
asyncFetcher := mcfetch.NewAsyncPlayerFetcher(
player,
memCache,
2,
2*time.Second,
5*time.Second,
)
asyncFetcher.FetchPlayerData(resultChan, errorChan)
}
var userID string
for i := 0; i < len(players); i++ {
select {
case data := <-resultChan:
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")
}
// Fetch player stats
res, err := api.GetPlayerResponse(userID)
if err != nil {
log.Fatalf("Failed to get player data: %v", err)
}
// Calculate derived stats
res.Player.Stats.Bedwars.WLR = calculateRatio(res.Player.Stats.Bedwars.Wins, res.Player.Stats.Bedwars.Losses)
res.Player.Stats.Bedwars.KDR = calculateRatio(res.Player.Stats.Bedwars.Kills, res.Player.Stats.Bedwars.Deaths)
res.Player.Stats.Bedwars.FKDR = calculateRatio(res.Player.Stats.Bedwars.FinalKills, res.Player.Stats.Bedwars.FinalDeaths)
res.Player.Stats.Bedwars.BBLR = calculateRatio(res.Player.Stats.Bedwars.BedsBroken, res.Player.Stats.Bedwars.BedsLost)
// Convert player struct to JSON
playerJSON, err := json.Marshal(res)
if err != nil {
log.Fatalf("Failed to marshal player data: %v", err)
}
// Append the player JSON to the stuff list
var playerMap map[string]interface{}
json.Unmarshal(playerJSON, &playerMap)
stuff = append(stuff, playerMap)
case err := <-errorChan:
log.Fatal(err)
}
}
println("UUID Done!")
println("Sending data...")
// Send the list of JSON objects to the client
message, err := json.Marshal(stuff)
if err != nil {
log.Fatalf("Failed to marshal stuff: %v", err)
}
err = client.SendData(string(message))
if err != nil {
log.Printf("Error sending data: %v", err)
}
fmt.Println("Sent stuff:", stuff)
println("Sending Done!")
}
select {
case <-client.DataChannel: // This blocks the main goroutine until data is received
fmt.Println("Data received, exiting...")
}
fmt.Println("Closing app")
}
// calculateRatio is a helper function to calculate ratios (e.g., WLR, KDR, etc.)
func calculateRatio(numerator, denominator int) float64 {
if denominator == 0 {
return float64(numerator)
}
return float64(numerator) / float64(denominator)
}

View File

@ -1,75 +1,239 @@
package client
import (
"bufio"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"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
const PORT = ":5518"
type PacketID int
const (
CONNECT_REQUEST PacketID = iota
CONNECT_RESPONSE
JOIN_REQUEST
JOIN_RESPONSE
CREATE_REQUEST
CREATE_RESPONSE
READ_DATA
SEND_DATA_REQUEST
SEND_DATA_RESPONSE
LEAVE_ROOM
)
// Client represents a client connection to the server.
type Client struct {
Conn net.Conn
ClientID string
RoomCode string
DataChannel chan string
}
// Read input from the terminal and send it to the server
func readInputAndSend(conn net.Conn) {
reader := bufio.NewReader(os.Stdin)
// NewClient creates a new client and connects to the server.
func NewClient(serverAddr, clientID string) (*Client, error) {
conn, err := net.Dial("tcp", serverAddr+PORT)
if err != nil {
return nil, fmt.Errorf("failed to connect to server: %w", err)
}
return &Client{
Conn: conn,
ClientID: clientID,
DataChannel: make(chan string),
}, nil
}
// CreateRoom creates a new room on the server.
func (c *Client) CreateRoom(password string) (string, error) {
request := CreateRoomRequestPacket{
UserID: c.ClientID,
Password: password,
}
if err := c.sendPacket(CREATE_REQUEST, request); err != nil {
return "", err
}
var response CreateRoomResponsePacket
if err := c.receivePacket(CREATE_RESPONSE, &response); err != nil {
return "", err
}
if !response.Success {
return "", errors.New(response.Reason)
}
c.RoomCode = response.RoomCode
return response.RoomCode, nil
}
// JoinRoom joins an existing room on the server.
func (c *Client) JoinRoom(roomCode, password string) error {
request := JoinRequestPacket{
UserID: c.ClientID,
RoomCode: roomCode,
Password: password,
}
if err := c.sendPacket(JOIN_REQUEST, request); err != nil {
return err
}
var response JoinRequestResponsePacket
if err := c.receivePacket(JOIN_RESPONSE, &response); err != nil {
return err
}
if !response.Success {
return errors.New(response.Reason)
}
c.RoomCode = roomCode
return nil
}
func (c *Client) ListenForData() {
go func() {
for {
fmt.Print("> ")
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
// Send the command to the server
_, err := conn.Write([]byte(text + "\n"))
var dataPacket SendDataRequestPacket
err := c.receivePacket(READ_DATA, &dataPacket)
if err != nil {
fmt.Println("Error sending message:", err)
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
}
// If the user types 'quit', exit the program
if text == "quit" {
fmt.Println("Goodbye!")
return
}
// Send the received data to the channel
c.DataChannel <- dataPacket.Data
}
}()
}
// 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
// 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,
}
fmt.Print(message)
if err := c.sendPacket(SEND_DATA_REQUEST, request); err != nil {
return fmt.Errorf("failed to send data packet: %w", err)
}
var response SendDataResponsePacket
if err := c.receivePacket(SEND_DATA_RESPONSE, &response); err != nil {
return fmt.Errorf("failed to receive response for sent data: %w", err)
}
if !response.Success {
return errors.New("server failed to process the data request: " + response.Reason)
}
return nil
}
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)
// LeaveRoom disconnects the client from the current room.
func (c *Client) LeaveRoom() error {
return c.sendPacket(LEAVE_ROOM, nil)
}
// Close closes the connection to the server.
func (c *Client) Close() {
c.Conn.Close()
}
// sendPacket sends a packet with a given ID and data to the server.
func (c *Client) sendPacket(packetID PacketID, data interface{}) error {
packetData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to encode packet: %w", err)
}
return writePacket(c.Conn, byte(packetID), packetData)
}
// receivePacket reads a response from the server for a given packet ID.
func (c *Client) receivePacket(expected PacketID, v interface{}) error {
packetID, data, err := readPacket(c.Conn)
if err != nil {
return err
}
if packetID != expected {
return fmt.Errorf("unexpected packet ID: got %v, want %v", packetID, expected)
}
return json.Unmarshal(data, v)
}
// readPacket reads a packet from the connection.
func readPacket(conn net.Conn) (PacketID, []byte, error) {
var length uint32
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return 0, nil, err
}
var messageType byte
if err := binary.Read(conn, binary.BigEndian, &messageType); err != nil {
return 0, nil, err
}
data := make([]byte, length-5)
if _, err := io.ReadFull(conn, data); err != nil {
return 0, nil, err
}
return PacketID(messageType), data, nil
}
// writePacket writes a packet to the connection.
func writePacket(conn net.Conn, messageType byte, data []byte) error {
length := uint32(5 + len(data))
if err := binary.Write(conn, binary.BigEndian, length); err != nil {
return err
}
if err := binary.Write(conn, binary.BigEndian, messageType); err != nil {
return err
}
_, err := conn.Write(data)
return err
}
type CreateRoomRequestPacket struct {
UserID string
Password string
}
type CreateRoomResponsePacket struct {
Success bool
Reason string
RoomCode string
}
type JoinRequestPacket struct {
UserID string
RoomCode string
Password string
}
type JoinRequestResponsePacket struct {
Success bool
Reason string
CurrentData string
}
type SendDataRequestPacket struct {
UserID string
Data string
}
type SendDataResponsePacket struct {
Success bool
Reason string
}

11
go.mod
View File

@ -1 +1,12 @@
module hudly
go 1.23.0
require (
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a
github.com/google/uuid v1.6.0
github.com/haleyrom/skia-go v0.0.0-20240328095045-3f321a6a6e4d
)
require github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f // indirect

View File

@ -1,201 +1,97 @@
package main
import (
"bufio"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"math/rand"
"net"
"strings"
"sync"
"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 {
conn net.Conn
username string
room *Room
Conn net.Conn
ClientID string
RoomCode string
}
type Room struct {
code string
password string
clients map[*Client]bool
lock sync.Mutex
Code string
Password string
Clients []*Client
}
var (
rooms = make(map[string]*Room)
mu sync.Mutex
)
var rooms = map[string]*Room{}
var clients = map[string]*Client{}
var 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))
func readPacket(conn net.Conn) (PacketID, []byte, error) {
// First, read the length of the packet (4 bytes)
var length uint32
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 handleClient(client *Client) {
defer client.conn.Close()
func writePacket(conn net.Conn, messageType byte, data []byte) error {
// Calculate the total length of the packet
// 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 {
return err
}
if err := binary.Write(conn, binary.BigEndian, messageType); err != nil {
return err
}
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))
}
// Write the data
_, err := conn.Write(data)
return err
}
// 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")
func main() {
listener, err := net.Listen("tcp", PORT)
if err != nil {
fmt.Println("Error starting server:", err)
return
}
defer listener.Close()
fmt.Println("Server started on port 5518")
fmt.Println("Server started on port", PORT)
for {
conn, err := listener.Accept()
@ -203,21 +99,332 @@ func startServer() {
fmt.Println("Error accepting connection:", err)
continue
}
go handleClient(conn)
}
}
go func() {
conn.Write([]byte("Enter your username: "))
username, _ := bufio.NewReader(conn).ReadString('\n')
username = strings.TrimSpace(username)
type ConnectionRequestPacket struct {
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:", err)
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) {
room := findRoom(packet.RoomCode)
if room == nil {
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: false,
Reason: "room does not exist",
}))
return
}
if room.Password == "" || packet.Password == room.Password {
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: true,
Reason: "",
}))
return
}
writePacket(conn, byte(CONNECT_RESPONSE), encodeResponsePacket(ConnectionResponsePacket{
Success: false,
Reason: "invalid password",
}))
}
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,
username: username,
Conn: conn,
ClientID: packet.UserID,
RoomCode: packet.RoomCode,
}
conn.Write([]byte(fmt.Sprintf("Welcome %s!\n", username)))
handleClient(client)
}()
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 main() {
startServer()
func handleSendData(conn net.Conn, packet SendDataRequestPacket) {
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 := findRoom(leavingClient.RoomCode)
if room == nil {
fmt.Println("Room not found")
return
}
for i, client := range room.Clients {
if client == leavingClient {
// Remove the client from the list
room.Clients = append(room.Clients[:i], room.Clients[i+1:]...)
break
}
}
// If the room is now empty or the leaving client was the creator, delete the room
if len(room.Clients) == 0 || room.Clients[0] == leavingClient {
for _, client := range room.Clients {
client.Conn.Close()
}
delete(rooms, room.Code)
fmt.Println("Room", room.Code, "deleted")
} else {
// // Notify remaining clients that a user has left (optional)
// for _, remainingClient := range room.Clients {
// writePacket(remainingClient.Conn, byte(LEAVE_ROOM), []byte(fmt.Sprintf("User %s has left the room", leavingClient.ClientID)))
// }
}
delete(clients, leavingClient.ClientID)
leavingClient.Conn.Close()
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
}