Compare commits

..

No commits in common. "master" and "get-lobby-from-passphrase" have entirely different histories.

22 changed files with 355 additions and 468 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/main.go"
}
]
}

View File

@ -1,83 +0,0 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"log"
"mchess_server/api"
"mchess_server/utils"
"net/http"
"mchess_server/lobbies"
"github.com/gin-gonic/gin"
gorillaws "github.com/gorilla/websocket"
)
var upgrader = gorillaws.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func RegisterWebSocketConnection(c *gin.Context) {
limiter.Take()
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println(err)
return
}
go waitForAndHandlePlayerID(c, conn)
}
func waitForAndHandlePlayerID(ctx context.Context, conn *gorillaws.Conn) {
limiter.Take()
msgType, msg, err := conn.ReadMessage()
if err != nil {
errorMessage := fmt.Sprintf("Reading from websocket connection did not work: %s", err)
log.Println(errorMessage)
conn.Close()
return
}
log.Println("read from websocket endpoint: ", msgType, string(msg), err)
var info api.PlayerInfo
err = json.Unmarshal(msg, &info)
if err != nil {
errorMessage := fmt.Sprintf("Unmarshaling message did not work: %s", err)
log.Println(errorMessage)
conn.WriteMessage(msgType, []byte(errorMessage))
conn.Close()
return
}
lobby := lobbies.GetLobbyRegistry().GetLobbyByPassphrase(utils.NewPassphraseFromString(*info.Passphrase))
if lobby == nil {
conn.WriteMessage(msgType, []byte("lobby not found"))
conn.Close()
return
}
player, found := lobby.GetPlayerByUUID(*info.PlayerID)
if !found {
conn.WriteMessage(msgType, []byte("player not found"))
conn.Close()
return
}
lobby.Game.SetWebsocketConnectionFor(ctx, player, conn)
log.Println("player after setting connection: ")
log.Println("id: ", player.Uuid)
log.Println("color: ", player.GetColor())
log.Println("Connection: ", player.Conn.ID)
}
func ConnectWsForGame(c *gin.Context) {
limiter.Take()
}

7
api/lobby_info.go Normal file
View File

@ -0,0 +1,7 @@
package api
import "github.com/google/uuid"
type LobbyInfo struct {
ID *uuid.UUID `json:"id,omitempty"`
}

View File

@ -4,5 +4,6 @@ import "github.com/google/uuid"
type PlayerInfo struct {
PlayerID *uuid.UUID `json:"playerID,omitempty"`
LobbyID *uuid.UUID `json:"lobbyID,omitempty"`
Passphrase *string `json:"passphrase,omitempty"`
}

View File

@ -296,34 +296,3 @@ func Test_Board_BlackCastlesRight_NoRook(t *testing.T) {
assert.False(t, valid)
}
func Test_Board_fixBugThatPreventsCastling(t *testing.T) {
board := newBoard()
board.Init()
//white move
valid, violation := board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 2}, EndSquare: types.Coordinate{Col: 5, Row: 3}})
assert.True(t, valid)
assert.Empty(t, violation)
//black move
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 7}, EndSquare: types.Coordinate{Col: 5, Row: 6}})
assert.True(t, valid)
assert.Empty(t, violation)
//white move
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 4, Row: 2}, EndSquare: types.Coordinate{Col: 4, Row: 3}})
assert.True(t, valid)
assert.Empty(t, violation)
//black move
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 6, Row: 7}, EndSquare: types.Coordinate{Col: 6, Row: 6}})
assert.True(t, valid)
assert.Empty(t, violation)
//queen moves to check the king
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 4, Row: 1}, EndSquare: types.Coordinate{Col: 8, Row: 5}})
assert.True(t, valid)
assert.Empty(t, violation)
//this moves should be valid but it was not because of a bug
valid, violation = board.CheckAndPlay(&types.Move{StartSquare: types.Coordinate{Col: 5, Row: 8}, EndSquare: types.Coordinate{Col: 5, Row: 7}})
assert.True(t, valid)
assert.Empty(t, violation)
}

View File

@ -8,7 +8,7 @@ import (
"mchess_server/types"
"github.com/google/uuid"
gorillaws "github.com/gorilla/websocket"
"nhooyr.io/websocket"
)
type Game struct {
@ -58,8 +58,8 @@ func (game Game) GetPlayer2() *Player {
}
func (game *Game) prepare() {
game.players[0].SetColor(types.White)
game.players[1].SetColor(types.Black)
game.players[0].color = types.White
game.players[1].color = types.Black
game.currentTurnPlayer = game.GetPlayer1()
@ -99,13 +99,13 @@ func (game *Game) Handle() {
game.gameState = PlayerToMove
case PlayerToMove:
log.Println("with ", game.currentTurnPlayer.GetColor(), " to move")
log.Println("with ", game.currentTurnPlayer.GetPlayerColor(), " to move")
receivedMove, err = game.currentTurnPlayer.ReadMove()
if err != nil {
log.Println("Error while reading message:", err)
return
}
log.Println("Player ", game.currentTurnPlayer.color.String(), " moved:\n", receivedMove)
log.Println("Player ", game.currentTurnPlayer, " moved:\n", receivedMove)
game.gameState = CheckMove
case CheckMove:
@ -117,7 +117,7 @@ func (game *Game) Handle() {
log.Println("Error marshalling 'colorDetermined' message for player 1", err)
return
}
game.currentTurnPlayer.writeMessage(string(invalidMoveMessage))
game.currentTurnPlayer.writeMessage(invalidMoveMessage)
game.gameState = PlayerToMove
continue
}
@ -157,14 +157,6 @@ func (game *Game) AddPlayersToGame(player *Player) {
game.players = append(game.players, player)
}
func (game *Game) AreBothPlayersConnected() bool {
if len(game.GetPlayers()) < 2 {
return false
}
return game.players[0].hasWebsocketConnection() && game.players[1].hasWebsocketConnection()
}
func (game *Game) killGame() {
log.Println("Game should be killed")
}
@ -181,22 +173,21 @@ func (game Game) notifyPlayersAboutGameStart() error {
return err
}
game.GetPlayer1().writeMessage(string(colorDeterminedPlayer1))
game.GetPlayer1().writeMessage(colorDeterminedPlayer1)
game.GetPlayer1().SendBoardState(types.Move{}, game.board.PGN(), types.White)
game.GetPlayer2().writeMessage(string(colorDeterminedPlayer2))
game.GetPlayer2().writeMessage(colorDeterminedPlayer2)
game.GetPlayer2().SendBoardState(types.Move{}, game.board.PGN(), types.White)
return nil
}
func (game Game) broadcastMove(move types.Move) error {
log.Println("broadcast move")
err := game.GetPlayer1().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.GetColor())
err := game.GetPlayer1().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.color)
if err != nil {
return err
}
err = game.GetPlayer2().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.GetColor())
err = game.GetPlayer2().SendBoardState(move, game.board.PGN(), game.currentTurnPlayer.color)
if err != nil {
return err
}
@ -220,10 +211,6 @@ func (game Game) broadcastGameEnd(reason GameEndedReason) error {
func (game *Game) playerDisconnected(p *Player) {
}
func (game *Game) SetWebsocketConnectionFor(ctx context.Context, p *Player, ws *gorillaws.Conn) {
p.SetWebsocketConnectionAndSendBoardState(ctx, ws, &game.board)
}
func (game *Game) SendBoardStateTo(p *Player) {
p.SendBoardState(game.board.getLastMove(), game.board.PGN(), game.board.colorToMove)
func (game *Game) SetWebsocketConnectionFor(ctx context.Context, p *Player, ws *websocket.Conn) {
p.SetWebsocketConnectionAndSendBoardState(ctx, ws, game.board.PGN(), game.board.colorToMove)
}

View File

@ -30,10 +30,6 @@ func (k King) AfterMoveAction(board *Board, fromSquare types.Coordinate) {
}
func (k King) HandleCastling(board *Board, move types.Move) (bool, Violation) {
if !k.isMoveCastlingMove(board, move) {
return false, ""
}
if k.hadMovedBefore(board) {
return false, ""
}
@ -70,28 +66,6 @@ const (
CastlingLeft CastlingDirection = "left"
)
func (k King) isMoveCastlingMove(b *Board, move types.Move) bool {
var destinationSquareForKingRight types.Coordinate
var destinationSquareForKingLeft types.Coordinate
switch k.Color {
case types.White:
destinationSquareForKingRight = types.Coordinate{Col: 7, Row: 1}
destinationSquareForKingLeft = types.Coordinate{Col: 3, Row: 1}
case types.Black:
destinationSquareForKingRight = types.Coordinate{Col: 7, Row: 8}
destinationSquareForKingLeft = types.Coordinate{Col: 3, Row: 8}
}
if move.EndSquare == destinationSquareForKingRight ||
move.EndSquare == destinationSquareForKingLeft {
return true
}
return false
}
func (k King) isCastlingAllowed(b *Board, move types.Move) (bool, CastlingDirection) {
var valid = false

View File

@ -10,7 +10,7 @@ import (
"mchess_server/types"
"github.com/google/uuid"
gorillaws "github.com/gorilla/websocket"
"nhooyr.io/websocket"
)
type Player struct {
@ -34,27 +34,18 @@ func (p Player) hasWebsocketConnection() bool {
return p.Conn.HasWebsocketConnection()
}
func (p *Player) SetWebsocketConnection(ctx context.Context, ws *gorillaws.Conn) {
func (p *Player) SetWebsocketConnection(ctx context.Context, ws *websocket.Conn) {
p.Conn.SetWebsocketConnection(ws)
p.Conn.SetForColor(p.color)
}
func (p *Player) SetWebsocketConnectionAndSendBoardState(
ctx context.Context,
ws *gorillaws.Conn,
board *Board,
ws *websocket.Conn,
boardPosition string,
turnColor types.ChessColor,
) {
p.SetWebsocketConnection(ctx, ws)
p.SendBoardState(board.getLastMove(), board.PGN(), board.colorToMove)
}
func (p *Player) SetColor(color types.ChessColor) {
p.color = color
p.Conn.SetForColor(p.color)
}
func (p *Player) GetColor() types.ChessColor {
return p.color
p.SendBoardState(types.Move{}, boardPosition, turnColor)
}
func (p *Player) SetDisconnectCallback(cb func(*Player)) {
@ -72,8 +63,8 @@ func (p *Player) IsInGame() bool {
}
func (p *Player) SendBoardState(move types.Move, boardPosition string, turnColor types.ChessColor) error {
var pColor = p.GetColor()
if p.GetColor() == "" { // we default to white if we do not know the color yet
var pColor = p.color
if p.color == "" { // we default to white if we do not know the color yet
pColor = types.White
}
@ -89,8 +80,11 @@ func (p *Player) SendBoardState(move types.Move, boardPosition string, turnColor
return err
}
p.writeMessage(string(messageToSend))
err = p.writeMessage(messageToSend)
if err != nil {
log.Println("Error during message writing:", err)
return err
}
return nil
}
@ -105,8 +99,11 @@ func (p *Player) SendMoveAndPosition(move types.Move, boardPosition string) erro
return err
}
p.writeMessage(string(messageToSend))
err = p.writeMessage(messageToSend)
if err != nil {
log.Println("Error during message writing:", err)
return err
}
return nil
}
@ -120,20 +117,26 @@ func (p *Player) SendGameEnded(reason GameEndedReason) error {
log.Println("Error while marshalling: ", err)
return err
}
p.writeMessage(string(messageToSend))
err = p.writeMessage(messageToSend)
if err != nil {
log.Println("Error during message writing:", err)
return err
}
return nil
}
func (p *Player) writeMessage(msg string) {
p.Conn.Write(msg)
func (p *Player) writeMessage(msg []byte) error {
return p.Conn.Write(msg)
}
func (p *Player) ReadMove() (types.Move, error) {
receivedMessage := p.readMessage()
receivedMessage, err := p.readMessage()
if err != nil {
return types.Move{}, err
}
var msg api.WebsocketMessage
err := json.Unmarshal(receivedMessage, &msg)
err = json.Unmarshal(receivedMessage, &msg)
if err != nil {
return types.Move{}, err
}
@ -145,9 +148,13 @@ func (p *Player) ReadMove() (types.Move, error) {
return *msg.Move, nil
}
func (p *Player) readMessage() []byte {
msg := p.Conn.Read()
log.Printf("Reading message from %s: %s", p.color.String(), string(msg))
func (p *Player) readMessage() ([]byte, error) {
msg, err := p.Conn.Read()
log.Printf("Reading message: %s from player %s", string(msg), p.Uuid.String())
return msg
return msg, err
}
func (p Player) GetPlayerColor() string {
return string(p.color)
}

View File

@ -11,7 +11,6 @@ var (
SomethingWentWrong Violation = "something went wrong"
CastlingThroughCheck Violation = "king would move through check"
CastlingWhileKingInCheck Violation = "king cannot castle while in check"
CastlingKingMovedBefore Violation = "king cannot caslte because he moved before"
)
func (v Violation) String() string {

View File

@ -50,7 +50,7 @@ func (b *MessageBuffer) Insert(msg string) {
b.cond.Broadcast()
}
func (b *MessageBuffer) Get() string {
func (b *MessageBuffer) Get() (string, error) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
@ -69,7 +69,7 @@ func (b *MessageBuffer) Get() string {
}
b.getIndex = b.incrementAndWrapIndex(b.getIndex)
return msg.content
return msg.content, nil
}
func (b MessageBuffer) incrementAndWrapIndex(index int) int {

View File

@ -66,7 +66,8 @@ func Test_MessageBuffer_GetWaitsForFirstData(t *testing.T) {
buf.Insert("delayed-message")
}()
msg := buf.Get()
msg, err := buf.Get()
assert.NoError(t, err)
endTime := time.Now()
@ -78,7 +79,8 @@ func Test_MessageBuffer_GetWaitsForNewData(t *testing.T) {
buf := newMessageBuffer(2)
buf.Insert("message-1")
msg := buf.Get()
msg, err := buf.Get()
assert.NoError(t, err)
assert.Equal(t, "message-1", msg)
go func() {
@ -87,7 +89,8 @@ func Test_MessageBuffer_GetWaitsForNewData(t *testing.T) {
buf.Insert("delayed-message")
}()
msg = buf.Get()
msg, err = buf.Get()
assert.NoError(t, err)
assert.Equal(t, "delayed-message", msg)
}
@ -114,7 +117,8 @@ func Test_MessageBuffer_IndexesAreCorrectAfterOverwritingOldData(t *testing.T) {
},
buf.messages)
msg := buf.Get()
msg, err := buf.Get()
assert.NoError(t, err)
assert.Equal(t, "message-2", msg)
}
@ -122,11 +126,13 @@ func Test_MessageBuffer_GetWaitsForNewDataIfOldOneWasAlreadyGotten(t *testing.T)
buf := newMessageBuffer(2)
buf.Insert(message1)
msg := buf.Get()
msg, err := buf.Get()
assert.NoError(t, err)
assert.Equal(t, message1, msg)
buf.Insert(message2)
msg = buf.Get()
msg, err = buf.Get()
assert.NoError(t, err)
assert.Equal(t, message2, msg)
go func() {
@ -134,7 +140,8 @@ func Test_MessageBuffer_GetWaitsForNewDataIfOldOneWasAlreadyGotten(t *testing.T)
buf.Insert(message3)
}()
msg = buf.Get()
msg, err = buf.Get()
assert.NoError(t, err)
assert.Equal(t, message3, msg)
}
@ -150,8 +157,9 @@ func Test_MessageBuffer_InsertCatchesUpWithRead(t *testing.T) {
buf.Insert(message6)
buf.Insert(message7)
msg := buf.Get()
msg, err := buf.Get()
assert.NoError(t, err)
assert.Equal(t, message3, msg)
}
@ -164,7 +172,7 @@ func Test_MessageBuffer_FuckShitUp(t *testing.T) {
var readMsg = make([]string, 0)
go func() {
for i := 0; i < size*10; i++ {
msg := buf.Get()
msg, _ := buf.Get()
if msg == "99" {
break
}

View File

@ -3,38 +3,22 @@ package connection
import (
"context"
"log"
"mchess_server/types"
"sync"
"github.com/google/uuid"
gorillaws "github.com/gorilla/websocket"
"nhooyr.io/websocket"
)
type Connection struct {
ID uuid.UUID
ws *gorillaws.Conn
ctx context.Context
rxBuffer *MessageBuffer
txBuffer *MessageBuffer
disconnectCallback func()
forColor types.ChessColor
/* this is a hack since a user using the same
browser might get the same object of type Connection in two instances of
the same browser. Then if Close() gets called for both of these instances
the first Close() will set ws=nil, and the seconds one will dereference this
nil pointer. The correct way to fix this, is to make sure, two instances
of the same browser do not get the same connection instance.
At the moment, the usage of localStorage 'identifies' an already connected
player and reuses the old Connection struct instance */
closingMutex sync.Mutex
ws *websocket.Conn
wsConnectionEstablished chan bool
ctx context.Context
buffer MessageBuffer
disconnectCallback func()
}
func NewConnection(options ...func(*Connection)) *Connection {
connection := Connection{
ID: uuid.New(),
rxBuffer: newMessageBuffer(100),
txBuffer: newMessageBuffer(100),
buffer: *newMessageBuffer(100),
wsConnectionEstablished: make(chan bool),
}
for _, option := range options {
@ -44,7 +28,7 @@ func NewConnection(options ...func(*Connection)) *Connection {
return &connection
}
func WithWebsocket(ws *gorillaws.Conn) func(*Connection) {
func WithWebsocket(ws *websocket.Conn) func(*Connection) {
return func(c *Connection) {
c.ws = ws
}
@ -64,10 +48,6 @@ func WithDisconnectCallback(cb func()) func(*Connection) {
}
}
func (conn *Connection) SetForColor(color types.ChessColor) {
conn.forColor = color
}
func (conn *Connection) SetDisconnectCallback(cb func()) {
conn.disconnectCallback = cb
}
@ -76,81 +56,53 @@ func (conn *Connection) HasWebsocketConnection() bool {
return conn.ws != nil
}
func (conn *Connection) readFromRxBuffer() {
for {
_, msg, err := conn.ws.ReadMessage()
if err != nil {
conn.logConnection("while reading from websocket: %w", "ERROR:", err.Error())
conn.Close("")
return
}
conn.rxBuffer.Insert(string(msg))
}
}
func (conn *Connection) writeTxBuffer() {
for {
msg := conn.txBuffer.Get()
if conn.ws == nil {
return
}
err := conn.ws.WriteMessage(gorillaws.TextMessage, []byte(msg))
if err != nil {
conn.logConnection("while writing to websocket: %w", "ERROR:", err.Error())
return
}
}
}
func (conn *Connection) SetWebsocketConnection(ws *gorillaws.Conn) {
func (conn *Connection) SetWebsocketConnection(ws *websocket.Conn) {
if ws == nil {
conn.logConnection("ERROR: setting ws = null")
return
}
conn.ws = ws
go conn.readFromRxBuffer()
go conn.writeTxBuffer()
select {
case conn.wsConnectionEstablished <- true:
default:
}
defer conn.logConnection("websocket connection set")
go func() {
for {
_, msg, err := conn.ws.Read(conn.ctx)
if err != nil {
log.Println("while reading from websocket: %w", err)
if conn.disconnectCallback != nil {
conn.disconnectCallback()
}
return
}
conn.buffer.Insert(string(msg))
}
}()
}
func (conn *Connection) Write(msg string) {
conn.logConnection("Writing message: ", string(msg))
conn.txBuffer.Insert(msg)
func (conn *Connection) Write(msg []byte) error {
if conn.ws == nil { //if ws is not yet set, we wait for it
<-conn.wsConnectionEstablished
}
log.Printf("Writing message: %s", string(msg))
return conn.ws.Write(conn.ctx, websocket.MessageText, msg)
}
func (conn *Connection) Read() []byte {
msg := conn.rxBuffer.Get()
func (conn *Connection) Read() ([]byte, error) {
msg, err := conn.buffer.Get()
if err != nil {
conn.ws = nil
return nil, err // TODO: Tell game-handler that connection was lost
}
return []byte(msg)
return []byte(msg), err
}
func (conn *Connection) Close(msg string) {
/* This is a hack, do not try this at home.
See more details at the definition of Connection */
conn.closingMutex.Lock()
defer conn.closingMutex.Unlock()
if conn == nil || conn.ws == nil {
return
}
conn.logConnection("closing websocket connection")
conn.ws.WriteMessage(gorillaws.TextMessage, []byte(msg))
conn.ws.Close()
conn.ws.Close(websocket.StatusCode(400), msg)
conn.ws = nil
conn.txBuffer.Insert("we do this to make txBuffer.Get() return")
}
func (con *Connection) logConnection(v ...string) {
a := ""
for _, s := range v {
a += s + ", "
}
log.Println(a, "on connection ", con.ID, ", for color ", con.forColor.String())
}

29
go.mod
View File

@ -5,27 +5,25 @@ go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/samber/lo v1.41.0
github.com/samber/lo v1.39.0
github.com/stretchr/testify v1.9.0
nhooyr.io/websocket v1.8.11
)
require github.com/benbjohnson/clock v1.3.5 // indirect
require (
github.com/bytedance/sonic v1.11.9 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gorilla/websocket v1.5.3
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -34,13 +32,12 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/ratelimit v0.3.1
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

78
go.sum
View File

@ -1,11 +1,18 @@
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
@ -15,10 +22,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -27,28 +34,23 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
@ -59,14 +61,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.41.0 h1:vZEqVQNmX/ykONfla8tiW6cdoLZxb3+LCZ63B8z/2eE=
github.com/samber/lo v1.41.0/go.mod h1:w7R6fO7h2lrnx/s0bWcZ55vXJI89p5UPM6+kyDL373E=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -83,43 +87,47 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
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/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,26 +1,24 @@
package handler
import (
"log"
"mchess_server/api"
"mchess_server/chess"
"mchess_server/lobbies"
lobbies "mchess_server/lobby_registry"
"mchess_server/usher"
"mchess_server/utils"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/ratelimit"
)
var mut sync.Mutex
var limiter = ratelimit.New(10)
func HostGameHandler(c *gin.Context) {
limiter.Take()
func HostPrivateGameHandler(c *gin.Context) {
player := chess.NewPlayer(uuid.New())
u := lobbies.GetUsher()
u := usher.GetUsher()
mut.Lock()
defer mut.Unlock()
@ -30,6 +28,7 @@ func HostGameHandler(c *gin.Context) {
passphrase := lobby.Passphrase.String()
info := api.PlayerInfo{
PlayerID: &player.Uuid,
LobbyID: &lobby.Uuid,
Passphrase: &passphrase,
}
c.Header("Access-Control-Allow-Origin", "*")
@ -37,8 +36,6 @@ func HostGameHandler(c *gin.Context) {
}
func GetLobbyForPassphraseHandler(c *gin.Context) {
limiter.Take()
reqPassphrase := c.Param("phrase")
if reqPassphrase == "" {
c.IndentedJSON(http.StatusBadRequest, reqPassphrase)
@ -53,44 +50,59 @@ func GetLobbyForPassphraseHandler(c *gin.Context) {
return
}
passphrase := api.Passphrase{
Value: (*string)(&lobby.Passphrase),
lobbyInfo := api.LobbyInfo{
ID: &lobby.Uuid,
}
c.IndentedJSON(http.StatusOK, lobbyInfo)
}
func RegisterForRandomGame(c *gin.Context) {
player := chess.NewPlayer(uuid.New())
usher := usher.GetUsher()
mut.Lock()
defer mut.Unlock()
lobby := usher.WelcomeNewPlayer(player)
usher.AddPlayerToLobbyAndStartGameIfFull(player, lobby)
info := api.PlayerInfo{
PlayerID: &player.Uuid,
LobbyID: &lobby.Uuid,
}
log.Println("responding with info ", info)
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, passphrase)
c.IndentedJSON(http.StatusOK, info)
}
func JoinPrivateGame(c *gin.Context) {
limiter.Take()
req := api.PlayerInfo{}
log.Println(c.Request.Body)
err := c.ShouldBindJSON(&req)
if err != nil || req.Passphrase == nil || *req.Passphrase == "" {
c.IndentedJSON(http.StatusNotFound, req)
return
}
u := lobbies.GetUsher()
u := usher.GetUsher()
if req.PlayerID != nil { //is reconnect
if req.Passphrase != nil &&
*req.Passphrase != "" &&
req.PlayerID != nil &&
req.LobbyID != nil { //is reconnect
lobby := u.FindExistingPrivateLobby(utils.Passphrase(*req.Passphrase))
var found bool
if lobby != nil {
_, found = lobby.GetPlayerByUUID(*req.PlayerID)
}
_, found := lobby.GetPlayerByUUID(*req.PlayerID)
if found {
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(
http.StatusOK,
api.PlayerInfo{
PlayerID: req.PlayerID,
LobbyID: req.LobbyID,
Passphrase: req.Passphrase,
})
return
} else {
c.IndentedJSON(http.StatusNotFound, req)
return
}
}
@ -108,32 +120,9 @@ func JoinPrivateGame(c *gin.Context) {
info := api.PlayerInfo{
PlayerID: &player.Uuid,
LobbyID: &lobby.Uuid,
Passphrase: req.Passphrase,
}
c.Header("Access-Control-Allow-Origin", "*")
c.IndentedJSON(http.StatusOK, info)
}
func JoinGameHandler(c *gin.Context) {
limiter.Take()
passphrase := api.Passphrase{}
err := c.ShouldBindJSON(&passphrase)
if err != nil {
c.IndentedJSON(http.StatusBadRequest, nil)
return
}
u := lobbies.GetUsher()
lobby := u.FindExistingPrivateLobby(utils.Passphrase(*passphrase.Value))
if lobby == nil {
c.IndentedJSON(http.StatusNotFound, nil)
return
}
lobbyInfo := api.Passphrase{
Value: (*string)(&lobby.Passphrase),
}
c.IndentedJSON(http.StatusOK, lobbyInfo)
}

View File

@ -9,17 +9,18 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func Test_GetLobbyFromPassphraseHandler(t *testing.T) {
var passphraseURLParameter string
var receivedPhrase string
var hostedLobbyId uuid.UUID
t.Run("host a lobby", func(t *testing.T) {
r1 := httptest.NewRecorder()
ctx1, e1 := gin.CreateTestContext(r1)
e1.GET("/api/hostPrivate", HostGameHandler)
e1.GET("/api/hostPrivate", HostPrivateGameHandler)
hostGameRequest, _ := http.NewRequest("GET", "/api/hostPrivate", nil)
ctx1.Request = hostGameRequest
@ -29,7 +30,8 @@ func Test_GetLobbyFromPassphraseHandler(t *testing.T) {
err := json.Unmarshal(r1.Body.Bytes(), &playerInfo)
assert.NoError(t, err)
receivedPhrase = *playerInfo.Passphrase
receivedPhrase := *playerInfo.Passphrase
hostedLobbyId = *playerInfo.LobbyID
passphrase := utils.NewPassphraseFromString(receivedPhrase)
passphraseURLParameter = passphrase.AsURLParam()
@ -46,10 +48,10 @@ func Test_GetLobbyFromPassphraseHandler(t *testing.T) {
engine.ServeHTTP(r2, getLobbyRequest)
passhrase := api.Passphrase{}
err := json.Unmarshal(r2.Body.Bytes(), &passhrase)
lobbyInfo := api.LobbyInfo{}
err := json.Unmarshal(r2.Body.Bytes(), &lobbyInfo)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, r2.Code)
assert.Equal(t, hostedLobbyId, *lobbyInfo.ID)
})
}

View File

@ -1,38 +0,0 @@
package lobbies
import (
"mchess_server/chess"
"mchess_server/utils"
)
type Usher struct {
}
var usherInstance *Usher
func newUsher() *Usher {
return &Usher{}
}
func GetUsher() *Usher {
if usherInstance == nil {
usherInstance = newUsher()
}
return usherInstance
}
func (u *Usher) WelcomeNewPlayer(player *chess.Player) *Lobby {
return GetLobbyRegistry().GetLobbyForPlayer()
}
func (u *Usher) CreateNewPrivateLobby(player *chess.Player) *Lobby {
return GetLobbyRegistry().CreateNewPrivateLobby()
}
func (u *Usher) FindExistingPrivateLobby(p utils.Passphrase) *Lobby {
return GetLobbyRegistry().GetLobbyByPassphrase(p)
}
func (u *Usher) AddPlayerToLobbyAndStartGameIfFull(player *chess.Player, lobby *Lobby) {
lobby.AddPlayerAndStartGameIfFull(player)
}

View File

@ -1,4 +1,4 @@
package lobbies
package lobby_registry
import (
"mchess_server/chess"
@ -8,15 +8,17 @@ import (
)
type Lobby struct {
Uuid uuid.UUID
Game *chess.Game
Passphrase utils.Passphrase
Uuid uuid.UUID
Game *chess.Game
PlayerJoined chan bool
Passphrase utils.Passphrase
}
func NewEmptyLobbyWithUUID(uuid uuid.UUID) *Lobby {
return &Lobby{
Uuid: uuid,
Game: chess.NewGame(),
Uuid: uuid,
Game: chess.NewGame(),
PlayerJoined: make(chan bool),
}
}
@ -29,16 +31,12 @@ func newEmptyLobbyWithPassphrase() *Lobby {
func (l *Lobby) AddPlayerAndStartGameIfFull(player *chess.Player) {
l.Game.AddPlayersToGame(player)
if l.ContainsTwoPlayers() {
if l.IsFull() {
l.Game.StartHandling()
}
}
func (w *Lobby) AreBothPlayersConnected() bool {
return w.Game.AreBothPlayersConnected()
}
func (w *Lobby) ContainsTwoPlayers() bool {
func (w *Lobby) IsFull() bool {
return len(w.Game.GetPlayers()) == 2
}

View File

@ -1,4 +1,4 @@
package lobbies
package lobby_registry
import (
"mchess_server/utils"
@ -7,7 +7,7 @@ import (
)
type LobbyRegistry struct {
lobbies map[utils.Passphrase]*Lobby
lobbies map[uuid.UUID]*Lobby
}
var instance *LobbyRegistry
@ -21,7 +21,7 @@ func GetLobbyRegistry() *LobbyRegistry {
}
func newLobbyRegistry() *LobbyRegistry {
return &LobbyRegistry{lobbies: make(map[utils.Passphrase]*Lobby)}
return &LobbyRegistry{lobbies: make(map[uuid.UUID]*Lobby)}
}
func (r *LobbyRegistry) CreateNewPrivateLobby() *Lobby {
@ -32,7 +32,7 @@ func (r *LobbyRegistry) CreateNewPrivateLobby() *Lobby {
func (r *LobbyRegistry) GetLobbyForPlayer() *Lobby {
for _, lobby := range r.lobbies {
if !lobby.ContainsTwoPlayers() {
if !lobby.IsFull() {
return lobby
}
}
@ -42,6 +42,10 @@ func (r *LobbyRegistry) GetLobbyForPlayer() *Lobby {
return newLobby
}
func (r *LobbyRegistry) GetLobbyByUUID(uuid uuid.UUID) *Lobby {
return r.lobbies[uuid]
}
func (r *LobbyRegistry) GetLobbyByPassphrase(p utils.Passphrase) *Lobby {
for _, lobby := range r.lobbies {
if lobby.Passphrase == p {
@ -52,6 +56,6 @@ func (r *LobbyRegistry) GetLobbyByPassphrase(p utils.Passphrase) *Lobby {
}
func (r *LobbyRegistry) addNewLobby(lobby *Lobby) uuid.UUID {
r.lobbies[lobby.Passphrase] = lobby
r.lobbies[lobby.Uuid] = lobby
return lobby.Uuid
}

64
main.go
View File

@ -1,11 +1,17 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"mchess_server/api/handler"
"mchess_server/api"
"mchess_server/handler"
lobbies "mchess_server/lobby_registry"
"github.com/gin-gonic/gin"
"nhooyr.io/websocket"
)
var cert_path = "/etc/letsencrypt/live/chess.sw-gross.de/"
@ -13,7 +19,6 @@ var cert_file = cert_path + "fullchain.pem"
var key_file = cert_path + "privkey.pem"
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
var debugMode bool
debugModeLong := flag.Bool("debug", false, "activates debug mode")
@ -24,13 +29,11 @@ func main() {
}
router := gin.Default()
router.GET("/api/hostPrivate", handler.HostGameHandler)
router.GET("/api/random", handler.RegisterForRandomGame)
router.GET("/api/hostPrivate", handler.HostPrivateGameHandler)
router.POST("/api/joinPrivate", handler.JoinPrivateGame)
router.GET("/api/ws", handler.RegisterWebSocketConnection)
router.GET("/api/ws", registerWebSocketConnection)
router.GET("/api/getLobbyForPassphrase/:phrase", handler.GetLobbyForPassphraseHandler)
router.GET("/api/registerWsForGame/:id", handler.ConnectWsForGame)
router.POST("/api/joinGame/:id", handler.JoinGameHandler)
if debugMode {
log.Println("Starting service WITHOUT TLS")
@ -42,3 +45,50 @@ func main() {
log.Fatal(router.RunTLS("chess.sw-gross.de:9999", cert_file, key_file))
}
}
func registerWebSocketConnection(c *gin.Context) {
webSocketConn, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{OriginPatterns: []string{"chess.sw-gross.de", "localhost:*"}})
if err != nil {
log.Println(err)
return
}
go waitForAndHandlePlayerID(c, webSocketConn)
}
func waitForAndHandlePlayerID(ctx context.Context, conn *websocket.Conn) {
msgType, msg, err := conn.Read(ctx)
if err != nil {
errorMessage := fmt.Sprintf("Reading from websocket connection did not work: %s", err)
log.Println(errorMessage)
conn.Close(websocket.StatusCode(400), errorMessage)
return
}
log.Println("read from websocket endpoint: ", msgType, string(msg), err)
var info api.PlayerInfo
err = json.Unmarshal(msg, &info)
if err != nil {
errorMessage := fmt.Sprintf("Unmarshaling message did not work: %s", err)
log.Println(errorMessage)
conn.Close(websocket.StatusCode(400), errorMessage)
return
}
lobby := lobbies.GetLobbyRegistry().GetLobbyByUUID(*info.LobbyID)
if lobby == nil {
conn.Close(websocket.StatusCode(400), "lobby not found")
return
}
player, found := lobby.GetPlayerByUUID(*info.PlayerID)
if !found {
conn.Close(websocket.StatusCode(400), "player not found")
return
}
if player.Conn.HasWebsocketConnection() {
player.Conn.Close("closing existing connection")
}
lobby.Game.SetWebsocketConnectionFor(ctx, player, conn)
log.Println("player after setting connection: ", player)
}

View File

@ -16,10 +16,6 @@ func (c ChessColor) Opposite() ChessColor {
}
}
func (c ChessColor) String() string {
return string(c)
}
type AdditionalState struct {
BlackKingMoved bool
WhiteKingMoved bool

45
usher/usher.go Normal file
View File

@ -0,0 +1,45 @@
package usher
import (
"mchess_server/chess"
lobbies "mchess_server/lobby_registry"
"mchess_server/utils"
)
type Usher struct {
}
var instance *Usher
func newUsher() *Usher {
return &Usher{}
}
func GetUsher() *Usher {
if instance == nil {
instance = newUsher()
}
return instance
}
func (u *Usher) WelcomeNewPlayer(player *chess.Player) *lobbies.Lobby {
lobby := lobbies.GetLobbyRegistry().GetLobbyForPlayer()
return lobby
}
func (u *Usher) CreateNewPrivateLobby(player *chess.Player) *lobbies.Lobby {
lobby := lobbies.GetLobbyRegistry().CreateNewPrivateLobby()
return lobby
}
func (u *Usher) FindExistingPrivateLobby(p utils.Passphrase) *lobbies.Lobby {
lobby := lobbies.GetLobbyRegistry().GetLobbyByPassphrase(p)
if lobby == nil || lobby.IsFull() {
return nil
}
return lobby
}
func (u *Usher) AddPlayerToLobbyAndStartGameIfFull(player *chess.Player, lobby *lobbies.Lobby) {
lobby.AddPlayerAndStartGameIfFull(player)
}