Draft: big refactor
This commit is contained in:
29
server/backend/api/getLobbyUsers.go
Normal file
29
server/backend/api/getLobbyUsers.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
// GetLobbyUsers
|
||||
//
|
||||
// @Router /api/lobby/users [get]
|
||||
func (self *Handlers) GetLobbyUsers(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
|
||||
userStatuses, err := self.db.GetUserStatuses(ctx)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to get user statuses",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return bunrouter.JSON(w, userStatuses)
|
||||
}
|
||||
55
server/backend/api/getUser.go
Normal file
55
server/backend/api/getUser.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
type GetUserOutput struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// GetUser
|
||||
//
|
||||
// @Accept json
|
||||
// @Param username query string true "Username to be queried"
|
||||
// @Success 200
|
||||
// @Failure 400
|
||||
// @Router /api/user [get]
|
||||
func (self *Handlers) GetUser(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
|
||||
username := req.URL.Query().Get("username")
|
||||
if username == "" {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "must provide username",
|
||||
}
|
||||
}
|
||||
|
||||
user, err := self.db.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Message: "user not exist",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to get user",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return bunrouter.JSON(w, GetUserOutput{
|
||||
Username: user.Username,
|
||||
})
|
||||
}
|
||||
15
server/backend/api/handlers.go
Normal file
15
server/backend/api/handlers.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"gitea.konchin.com/ytshih/inp2025/game/interfaces"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/rooms"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
db interfaces.Database
|
||||
roomManager *rooms.RoomManager
|
||||
}
|
||||
|
||||
func NewHandlers(db interfaces.Database, roomManager *rooms.RoomManager) *Handlers {
|
||||
return &Handlers{db: db, roomManager: roomManager}
|
||||
}
|
||||
115
server/backend/api/rooms.go
Normal file
115
server/backend/api/rooms.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
func (self *Handlers) CreateRoom(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
user, ok := ctx.Value(types.User("")).(models.User)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "user not found",
|
||||
}
|
||||
}
|
||||
|
||||
room, err := self.roomManager.CreateRoom(user.Username)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to create room",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return bunrouter.JSON(w, room)
|
||||
}
|
||||
|
||||
func (self *Handlers) GetLobbyRooms(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
rooms := self.roomManager.GetRooms()
|
||||
return bunrouter.JSON(w, rooms)
|
||||
}
|
||||
|
||||
func (self *Handlers) JoinRoom(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
user, ok := ctx.Value(types.User("")).(models.User)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "user not found",
|
||||
}
|
||||
}
|
||||
|
||||
roomID := req.Param("id")
|
||||
room, err := self.roomManager.JoinRoom(roomID, user.Username)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: err.Error(),
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return bunrouter.JSON(w, room)
|
||||
}
|
||||
|
||||
func (self *Handlers) GetRoom(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
roomID := req.Param("id")
|
||||
room, ok := self.roomManager.GetRoom(roomID)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Message: "room not found",
|
||||
}
|
||||
}
|
||||
|
||||
return bunrouter.JSON(w, room)
|
||||
}
|
||||
|
||||
func (self *Handlers) WSGetRoomState(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
roomID := req.Param("id")
|
||||
handler, ok := self.roomManager.GetGameHandler(roomID)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Message: "game handler not found",
|
||||
}
|
||||
}
|
||||
return handler.WSGetState(w, req)
|
||||
}
|
||||
|
||||
func (self *Handlers) PostGuess(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
roomID := req.Param("id")
|
||||
handler, ok := self.roomManager.GetGameHandler(roomID)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Message: "game handler not found",
|
||||
}
|
||||
}
|
||||
return handler.WSPostGuess(w, req)
|
||||
}
|
||||
37
server/backend/auth/getLogin.go
Normal file
37
server/backend/auth/getLogin.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
func (self *Handlers) GetLogin(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
user, ok := ctx.Value(types.User("")).(models.User)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "user not found",
|
||||
}
|
||||
}
|
||||
|
||||
err := self.db.InsertUserStatus(ctx,
|
||||
models.UserStatus{Username: user.Username})
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to update user status",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
13
server/backend/auth/handlers.go
Normal file
13
server/backend/auth/handlers.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"gitea.konchin.com/ytshih/inp2025/game/interfaces"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
db interfaces.Database
|
||||
}
|
||||
|
||||
func NewHandlers(db interfaces.Database) *Handlers {
|
||||
return &Handlers{db: db}
|
||||
}
|
||||
40
server/backend/auth/postLogout.go
Normal file
40
server/backend/auth/postLogout.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
// PostLogout
|
||||
//
|
||||
// @Success 200
|
||||
// @Router /auth/logout [post]
|
||||
func (self *Handlers) PostLogout(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
|
||||
user, ok := ctx.Value(types.User("")).(models.User)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "missing user",
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.db.DeleteUserStatus(ctx, user.Username); err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to delete user status",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
62
server/backend/auth/postRegister.go
Normal file
62
server/backend/auth/postRegister.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
// PostRegister
|
||||
//
|
||||
// @Accept json
|
||||
// @Param request body models.User true "query params"
|
||||
// @Success 200
|
||||
// @Failure 400
|
||||
// @Router /auth/register [post]
|
||||
func (self *Handlers) PostRegister(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "failed to read body payload",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := json.Unmarshal(b, &user); err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "failed to unmarshal json into user",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.db.InsertUser(ctx, user); err != nil {
|
||||
if err == types.UsernameConflictError {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "username already exist",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to insert user",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
122
server/docs/docs.go
Normal file
122
server/docs/docs.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"license": {
|
||||
"name": "0BSD"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/api/lobby/users": {
|
||||
"get": {
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/api/user": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Username to be queried",
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "query params",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.User"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"models.User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BasicAuth": {
|
||||
"type": "basic"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "0.0.1-alpha",
|
||||
Host: "",
|
||||
BasePath: "/",
|
||||
Schemes: []string{},
|
||||
Title: "Intro. to Network Programming Game",
|
||||
Description: "",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
96
server/docs/swagger.json
Normal file
96
server/docs/swagger.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Intro. to Network Programming Game",
|
||||
"contact": {},
|
||||
"license": {
|
||||
"name": "0BSD"
|
||||
},
|
||||
"version": "0.0.1-alpha"
|
||||
},
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/api/lobby/users": {
|
||||
"get": {
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/api/user": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Username to be queried",
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "query params",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.User"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"models.User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BasicAuth": {
|
||||
"type": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
server/docs/swagger.yaml
Normal file
59
server/docs/swagger.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
basePath: /
|
||||
definitions:
|
||||
models.User:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact: {}
|
||||
license:
|
||||
name: 0BSD
|
||||
title: Intro. to Network Programming Game
|
||||
version: 0.0.1-alpha
|
||||
paths:
|
||||
/api/lobby/users:
|
||||
get:
|
||||
responses: {}
|
||||
/api/user:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: Username to be queried
|
||||
in: query
|
||||
name: username
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
"400":
|
||||
description: Bad Request
|
||||
/auth/logout:
|
||||
post:
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
/auth/register:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: query params
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.User'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
"400":
|
||||
description: Bad Request
|
||||
securityDefinitions:
|
||||
BasicAuth:
|
||||
type: basic
|
||||
swagger: "2.0"
|
||||
18
server/middlewares/accessLog.go
Normal file
18
server/middlewares/accessLog.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/tracing"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
func (self *Handlers) AccessLog(
|
||||
next bunrouter.HandlerFunc,
|
||||
) bunrouter.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req bunrouter.Request) error {
|
||||
tracing.Logger.
|
||||
Info(req.Method + " " + req.Params().Route())
|
||||
return next(w, req)
|
||||
}
|
||||
}
|
||||
56
server/middlewares/auth.go
Normal file
56
server/middlewares/auth.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/tracing"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (self *Handlers) Auth(
|
||||
next bunrouter.HandlerFunc,
|
||||
) bunrouter.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req bunrouter.Request) error {
|
||||
ctx := req.Context()
|
||||
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "basic auth wrong format",
|
||||
}
|
||||
}
|
||||
|
||||
dbUser, err := self.db.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "username not exist",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
if dbUser.Password != password {
|
||||
tracing.Logger.
|
||||
Debug("password input",
|
||||
zap.String("input.password", password),
|
||||
zap.String("dbuser.password", dbUser.Password))
|
||||
return HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "password incorrect",
|
||||
}
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, types.User(""), user)
|
||||
return next(w, req.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
50
server/middlewares/bunrouterOtel.go
Normal file
50
server/middlewares/bunrouterOtel.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func (self *Handlers) BunrouterOtel(
|
||||
next bunrouter.HandlerFunc,
|
||||
) bunrouter.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req bunrouter.Request) error {
|
||||
span := trace.SpanFromContext(req.Context())
|
||||
if !span.IsRecording() {
|
||||
return next(w, req)
|
||||
}
|
||||
|
||||
params := req.Params()
|
||||
span.SetName(req.Method + " " + params.Route())
|
||||
|
||||
paramSlice := params.Slice()
|
||||
attrs := make([]attribute.KeyValue, 0, 3+len(paramSlice))
|
||||
|
||||
attrs = append(attrs, semconv.HTTPRouteKey.String(req.Route()))
|
||||
|
||||
if req.URL != nil {
|
||||
attrs = append(attrs, semconv.HTTPTargetKey.String(req.URL.RequestURI()))
|
||||
} else {
|
||||
// This should never occur if the request was generated by the net/http
|
||||
// package. Fail gracefully, if it does though.
|
||||
attrs = append(attrs, semconv.HTTPTargetKey.String(req.RequestURI))
|
||||
}
|
||||
|
||||
attrs = append(attrs, semconv.HTTPClientIPKey.String(
|
||||
utils.GetRemoteAddr(req)))
|
||||
|
||||
for _, param := range paramSlice {
|
||||
attrs = append(attrs, attribute.String("http.route.param."+param.Key,
|
||||
param.Value))
|
||||
}
|
||||
|
||||
span.SetAttributes(attrs...)
|
||||
|
||||
return next(w, req)
|
||||
}
|
||||
}
|
||||
87
server/middlewares/errorHandler.go
Normal file
87
server/middlewares/errorHandler.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/tracing"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type HTTPError struct {
|
||||
StatusCode int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
OriginError error `json:"-"`
|
||||
}
|
||||
|
||||
func (e HTTPError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewHTTPError(err error) HTTPError {
|
||||
return HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "Internal server error with unknown reason",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Handlers) ErrorHandler(
|
||||
next bunrouter.HandlerFunc,
|
||||
) bunrouter.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req bunrouter.Request) error {
|
||||
err := next(w, req)
|
||||
ctx := req.Context()
|
||||
|
||||
var httpErr HTTPError
|
||||
switch err := err.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
|
||||
case HTTPError:
|
||||
httpErr = err
|
||||
|
||||
default:
|
||||
tracing.Logger.
|
||||
Error("unhandled error",
|
||||
zap.Error(err))
|
||||
httpErr = NewHTTPError(err)
|
||||
}
|
||||
|
||||
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.SetAttributes(
|
||||
attribute.Int("http.response.status_code", httpErr.StatusCode),
|
||||
)
|
||||
|
||||
if 500 <= httpErr.StatusCode && httpErr.StatusCode <= 599 {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
if httpErr.OriginError == nil {
|
||||
tracing.Logger.
|
||||
Error(httpErr.Message)
|
||||
} else {
|
||||
tracing.Logger.
|
||||
Error(httpErr.Message,
|
||||
zap.Error(httpErr.OriginError))
|
||||
}
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
if httpErr.OriginError == nil {
|
||||
tracing.Logger.
|
||||
Warn(httpErr.Message)
|
||||
} else {
|
||||
tracing.Logger.
|
||||
Warn(httpErr.Message,
|
||||
zap.Error(httpErr.OriginError))
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(httpErr.StatusCode)
|
||||
_ = bunrouter.JSON(w, httpErr)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
11
server/middlewares/handler.go
Normal file
11
server/middlewares/handler.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package middlewares
|
||||
|
||||
import "gitea.konchin.com/ytshih/inp2025/game/interfaces"
|
||||
|
||||
type Handlers struct {
|
||||
db interfaces.Database
|
||||
}
|
||||
|
||||
func NewHandlers(db interfaces.Database) *Handlers {
|
||||
return &Handlers{db: db}
|
||||
}
|
||||
93
server/rooms/manager.go
Normal file
93
server/rooms/manager.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package rooms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/wordle"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RoomManager struct {
|
||||
mu sync.RWMutex
|
||||
rooms map[string]*types.Room
|
||||
gameHandlers map[string]*wordle.Handlers
|
||||
}
|
||||
|
||||
func NewRoomManager() *RoomManager {
|
||||
return &RoomManager{
|
||||
rooms: make(map[string]*types.Room),
|
||||
gameHandlers: make(map[string]*wordle.Handlers),
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *RoomManager) CreateRoom(creater string) (*types.Room, error) {
|
||||
room := &types.Room{
|
||||
ID: uuid.New().String(),
|
||||
Creater: creater,
|
||||
Status: types.RoomStatusWaiting,
|
||||
Players: []string{creater},
|
||||
}
|
||||
|
||||
gameHandler := wordle.NewHandlers()
|
||||
|
||||
rm.mu.Lock()
|
||||
rm.rooms[room.ID] = room
|
||||
rm.gameHandlers[room.ID] = gameHandler
|
||||
rm.mu.Unlock()
|
||||
|
||||
return room, nil
|
||||
}
|
||||
|
||||
func (rm *RoomManager) GetRooms() []*types.Room {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
|
||||
rooms := make([]*types.Room, 0, len(rm.rooms))
|
||||
for _, room := range rm.rooms {
|
||||
rooms = append(rooms, room)
|
||||
}
|
||||
return rooms
|
||||
}
|
||||
|
||||
func (rm *RoomManager) GetRoom(id string) (*types.Room, bool) {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
room, ok := rm.rooms[id]
|
||||
return room, ok
|
||||
}
|
||||
|
||||
func (rm *RoomManager) GetGameHandler(id string) (*wordle.Handlers, bool) {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
handler, ok := rm.gameHandlers[id]
|
||||
return handler, ok
|
||||
}
|
||||
|
||||
func (rm *RoomManager) JoinRoom(id, username string) (*types.Room, error) {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
|
||||
room, ok := rm.rooms[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("room not found")
|
||||
}
|
||||
|
||||
if len(room.Players) >= 2 {
|
||||
return nil, fmt.Errorf("room is full")
|
||||
}
|
||||
|
||||
for _, player := range room.Players {
|
||||
if player == username {
|
||||
return room, nil // already in room
|
||||
}
|
||||
}
|
||||
|
||||
room.Players = append(room.Players, username)
|
||||
if len(room.Players) == 2 {
|
||||
room.Status = types.RoomStatusPlaying
|
||||
}
|
||||
|
||||
return room, nil
|
||||
}
|
||||
65
server/wordle/handlers.go
Normal file
65
server/wordle/handlers.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package wordle
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type OperationType int
|
||||
|
||||
const (
|
||||
OperationTypeGuess OperationType = iota
|
||||
)
|
||||
|
||||
type Operation struct {
|
||||
Type OperationType
|
||||
Username string
|
||||
Guess string
|
||||
}
|
||||
|
||||
type Handlers struct {
|
||||
state types.WordleState
|
||||
ans string
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
opCh chan Operation
|
||||
subs []*chan types.WordleState
|
||||
}
|
||||
|
||||
func NewHandlers() *Handlers {
|
||||
ret := &Handlers{
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
|
||||
subs: nil,
|
||||
}
|
||||
go ret.GameFlow()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (self *Handlers) GameFlow() {
|
||||
for op := range self.opCh {
|
||||
switch op.Type {
|
||||
case OperationTypeGuess:
|
||||
self.state.CurrentGuess++
|
||||
if self.state.CurrentGuess >= len(self.state.Users) {
|
||||
for username, user := range self.state.Users {
|
||||
user.History = append(user.History,
|
||||
types.NewWord(op.Guess, self.ans))
|
||||
self.state.Users[username] = user
|
||||
}
|
||||
|
||||
for _, sub := range self.subs {
|
||||
*sub <- self.state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
server/wordle/wsGetState.go
Normal file
44
server/wordle/wsGetState.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package wordle
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/utils"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
func (self *Handlers) WSGetState(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
c, err := self.upgrader.Upgrade(w, req.Request, nil)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to upgrade websocket",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
dataCh := make(chan types.WordleState)
|
||||
self.subs = append(self.subs, &dataCh)
|
||||
|
||||
for data := range dataCh {
|
||||
b, err := msgpack.Marshal(data)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to marshal data into msgpack",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
c.WriteMessage(websocket.BinaryMessage, b)
|
||||
}
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
38
server/wordle/wsPostGuess.go
Normal file
38
server/wordle/wsPostGuess.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package wordle
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/game/server/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/game/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
func (self *Handlers) WSPostGuess(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "failed to read body",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
var op Operation
|
||||
if err := msgpack.Unmarshal(b, &op); err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "failed to unmarshal msgpack",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
self.opCh <- op
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
Reference in New Issue
Block a user