From 85fa3dfe737292fd17fbf45b5ca9125317b7ec9f Mon Sep 17 00:00:00 2001 From: Yi-Ting Shih Date: Tue, 16 Sep 2025 16:03:27 +0800 Subject: [PATCH] Draft: big refactor --- Makefile | 3 +- backend/handlers/api/getLobbyRooms.go | 29 ---- backend/handlers/api/handlers.go | 13 -- cmd/play/root.go | 18 ++- cmd/serve/backend.go | 52 ++++--- docker-compose.yaml | 21 ++- docker/otelcol/config.yaml | 16 +-- go.mod | 45 +++--- go.sum | 85 +++++------- go.work.sum | 14 +- implements/bunDatabase.go | 31 ----- interfaces/database.go | 14 -- models/room.go | 47 ------- plays/base.go | 6 +- plays/game.go | 128 ++++++++++++++++++ plays/joinRoom.go | 89 ++++++++++++ plays/lobby.go | 104 +++++++------- plays/login.go | 1 + plays/register.go | 20 +-- plays/roomWaiting.go | 90 ++++++++++++ .../backend}/api/getLobbyUsers.go | 2 +- .../backend}/api/getUser.go | 2 +- server/backend/api/handlers.go | 15 ++ server/backend/api/rooms.go | 115 ++++++++++++++++ .../backend}/auth/getLogin.go | 2 +- .../backend}/auth/handlers.go | 0 .../backend}/auth/postLogout.go | 2 +- .../backend}/auth/postRegister.go | 2 +- {backend => server}/docs/docs.go | 5 - {backend => server}/docs/swagger.json | 5 - {backend => server}/docs/swagger.yaml | 3 - {backend => server}/middlewares/accessLog.go | 2 +- {backend => server}/middlewares/auth.go | 2 +- .../middlewares/bunrouterOtel.go | 0 .../middlewares/errorHandler.go | 10 +- {backend => server}/middlewares/handler.go | 0 server/rooms/manager.go | 93 +++++++++++++ server/wordle/handlers.go | 65 +++++++++ server/wordle/wsGetState.go | 44 ++++++ server/wordle/wsPostGuess.go | 38 ++++++ tracing/tracer.go | 37 +++-- types/game.go | 31 +++++ types/wordle.go | 89 ++++++++++++ utils/initDB.go | 1 - utils/readConfig.go | 2 +- utils/udp.go | 77 +++++++++++ workflows/wordleServer.go | 47 +++++++ 47 files changed, 1166 insertions(+), 351 deletions(-) delete mode 100644 backend/handlers/api/getLobbyRooms.go delete mode 100644 backend/handlers/api/handlers.go delete mode 100644 models/room.go create mode 100644 plays/game.go create mode 100644 plays/joinRoom.go create mode 100644 plays/roomWaiting.go rename {backend/handlers => server/backend}/api/getLobbyUsers.go (89%) rename {backend/handlers => server/backend}/api/getUser.go (94%) create mode 100644 server/backend/api/handlers.go create mode 100644 server/backend/api/rooms.go rename {backend/handlers => server/backend}/auth/getLogin.go (92%) rename {backend/handlers => server/backend}/auth/handlers.go (100%) rename {backend/handlers => server/backend}/auth/postLogout.go (93%) rename {backend/handlers => server/backend}/auth/postRegister.go (95%) rename {backend => server}/docs/docs.go (96%) rename {backend => server}/docs/swagger.json (95%) rename {backend => server}/docs/swagger.yaml (95%) rename {backend => server}/middlewares/accessLog.go (90%) rename {backend => server}/middlewares/auth.go (97%) rename {backend => server}/middlewares/bunrouterOtel.go (100%) rename {backend => server}/middlewares/errorHandler.go (92%) rename {backend => server}/middlewares/handler.go (100%) create mode 100644 server/rooms/manager.go create mode 100644 server/wordle/handlers.go create mode 100644 server/wordle/wsGetState.go create mode 100644 server/wordle/wsPostGuess.go create mode 100644 types/game.go create mode 100644 types/wordle.go create mode 100644 utils/udp.go create mode 100644 workflows/wordleServer.go diff --git a/Makefile b/Makefile index 0dbd2dc..619981e 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ SWAG ?= ~/go/bin/swag TARGET := $(shell find . -type f -name '*.go') +DEST := server/docs all: swagger docker install @@ -17,7 +18,7 @@ game: $(TARGET) swagger: $(SWAG) fmt - $(SWAG) init -o backend/docs -g cmd/serve/backend.go -pdl 1 + $(SWAG) init -o $(DEST) -g cmd/serve/backend.go -pdl 1 clean: -rm -f game diff --git a/backend/handlers/api/getLobbyRooms.go b/backend/handlers/api/getLobbyRooms.go deleted file mode 100644 index fa9f912..0000000 --- a/backend/handlers/api/getLobbyRooms.go +++ /dev/null @@ -1,29 +0,0 @@ -package api - -import ( - "net/http" - - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" - "github.com/uptrace/bunrouter" -) - -// GetLobbyRooms -// -// @Router /api/lobby/rooms [get] -func (self *Handlers) GetLobbyRooms( - w http.ResponseWriter, - req bunrouter.Request, -) error { - ctx := req.Context() - - rooms, err := self.db.GetRooms(ctx) - if err != nil { - return middlewares.HTTPError{ - StatusCode: http.StatusInternalServerError, - Message: "failed to get room", - OriginError: err, - } - } - - return bunrouter.JSON(w, rooms) -} diff --git a/backend/handlers/api/handlers.go b/backend/handlers/api/handlers.go deleted file mode 100644 index ee7da76..0000000 --- a/backend/handlers/api/handlers.go +++ /dev/null @@ -1,13 +0,0 @@ -package api - -import ( - "gitea.konchin.com/ytshih/inp2025/game/interfaces" -) - -type Handlers struct { - db interfaces.Database -} - -func NewHandlers(db interfaces.Database) *Handlers { - return &Handlers{db: db} -} diff --git a/cmd/play/root.go b/cmd/play/root.go index 3e06188..e2f43f5 100644 --- a/cmd/play/root.go +++ b/cmd/play/root.go @@ -6,6 +6,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/go-resty/resty/v2" "github.com/spf13/cobra" + "go.uber.org/zap" ) var RootCmd = &cobra.Command{ @@ -15,25 +16,35 @@ var RootCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { appname := "game-tui" tracing.InitTracer(appname) + defer tracing.DeferTracer() + + tracing.Logger. + Info("client up") client := resty.New(). SetBaseURL(args[0]). SetDisableWarn(true) queue := []*tea.Program{} - queue = append(queue, - tea.NewProgram(plays.NewLanding(plays.NewBase(client)))) + queue = append(queue, tea.NewProgram( + plays.NewLanding(plays.NewBase(client)))) for len(queue) > 0 { program := queue[0] queue = queue[1:] + tracing.Logger.Info("run program") + res, err := program.Run() if err != nil { + tracing.Logger.Error("program failed", + zap.Error(err)) panic(err) } err = res.(plays.Next).Next(&queue) if err != nil { + tracing.Logger.Error("failed to generate next program", + zap.Error(err)) panic(err) } } @@ -41,4 +52,7 @@ var RootCmd = &cobra.Command{ } func init() { + RootCmd.Flags(). + String("otel-endpoint", "localhost:4317", + "endpoint for otlp exporter to connect") } diff --git a/cmd/serve/backend.go b/cmd/serve/backend.go index 0ee1d24..b4d6e7d 100644 --- a/cmd/serve/backend.go +++ b/cmd/serve/backend.go @@ -5,6 +5,14 @@ import ( "log" "net/http" + "gitea.konchin.com/ytshih/inp2025/game/implements" + "gitea.konchin.com/ytshih/inp2025/game/server/backend/api" + "gitea.konchin.com/ytshih/inp2025/game/server/backend/auth" + _ "gitea.konchin.com/ytshih/inp2025/game/server/docs" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/server/rooms" + "gitea.konchin.com/ytshih/inp2025/game/tracing" + "gitea.konchin.com/ytshih/inp2025/game/utils" "github.com/spf13/cobra" "github.com/spf13/viper" httpSwagger "github.com/swaggo/http-swagger" @@ -15,36 +23,29 @@ import ( "github.com/uptrace/bunrouter" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.uber.org/zap" - - _ "gitea.konchin.com/ytshih/inp2025/game/backend/docs" - "gitea.konchin.com/ytshih/inp2025/game/backend/handlers/api" - "gitea.konchin.com/ytshih/inp2025/game/backend/handlers/auth" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" - "gitea.konchin.com/ytshih/inp2025/game/implements" - "gitea.konchin.com/ytshih/inp2025/game/tracing" - "gitea.konchin.com/ytshih/inp2025/game/utils" ) -// @title Intro. to Network Programming Game -// @version 0.0.1-alpha -// @license.name 0BSD -// @securityDefinitions.basic BasicAuth -// @BasePath / +// @title Intro. to Network Programming Game +// @version 0.0.1-alpha +// @license.name 0BSD +// @securityDefinitions.basic BasicAuth +// @BasePath / var backendCmd = &cobra.Command{ Use: "backend", Short: "Game backend", Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() tracing.InitTracer("game backend") + defer tracing.DeferTracer() - tracing.Logger.Ctx(ctx). + tracing.Logger. Debug("connect to sql", zap.String("sql.url", "file:"+viper.GetString("sqlite-file")+"?cache=shared&mode=rwc")) sqldb, err := sql.Open(sqliteshim.ShimName, "file:"+viper.GetString("sqlite-file")+"?cache=shared&mode=rwc") if err != nil { - tracing.Logger.Ctx(ctx). + tracing.Logger. Panic("failed to init sqlite", zap.Error(err)) panic(err) @@ -52,15 +53,16 @@ var backendCmd = &cobra.Command{ bunDB := bun.NewDB(sqldb, sqlitedialect.New()) bunDB.AddQueryHook(bunotel.NewQueryHook(bunotel.WithDBName("sqlite"))) if err := utils.InitDB(ctx, bunDB); err != nil { - tracing.Logger.Ctx(ctx). + tracing.Logger. Panic("failed to init db schema", zap.Error(err)) panic(err) } db := implements.NewBunDatabase(bunDB) + roomManager := rooms.NewRoomManager() authHandlers := auth.NewHandlers(db) - apiHandlers := api.NewHandlers(db) + apiHandlers := api.NewHandlers(db, roomManager) middlewareHandlers := middlewares.NewHandlers(db) router := bunrouter.New() @@ -90,8 +92,20 @@ var backendCmd = &cobra.Command{ Use(middlewareHandlers.Auth) apiGroup.GET("/lobby/users", apiHandlers.GetLobbyUsers) + apiGroup.GET("/lobby/rooms", + apiHandlers.GetLobbyRooms) + apiGroup.POST("/rooms", + apiHandlers.CreateRoom) + apiGroup.GET("/rooms/:id", + apiHandlers.GetRoom) + apiGroup.POST("/rooms/:id/join", + apiHandlers.JoinRoom) + apiGroup.GET("/rooms/:id/state", + apiHandlers.WSGetRoomState) + apiGroup.POST("/rooms/:id/guess", + apiHandlers.PostGuess) - tracing.Logger.Ctx(ctx). + tracing.Logger. Info("http server up", zap.String("http.port", viper.GetString("port"))) @@ -101,6 +115,8 @@ var backendCmd = &cobra.Command{ } func init() { + backendCmd.Flags(). + String("otel-endpoint", "backend-otelcol:4317", "otelcol endpoint") backendCmd.Flags(). String("port", "8080", "Port to listen on") backendCmd.Flags(). diff --git a/docker-compose.yaml b/docker-compose.yaml index 619921f..cd621c0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,9 +1,28 @@ --- services: + backend-otelcol: + image: otel/opentelemetry-collector-contrib + command: + - --config=/etc/otelcol/config.yaml + volumes: + - ./docker/otelcol/config.yaml:/etc/otelcol/config.yaml + backend: build: context: . - env_file: ./backend/.env + env_file: ./server/.env ports: - 8081:8080 restart: unless-stopped + depends_on: + - backend-otelcol + + client-otelcol: + image: otel/opentelemetry-collector-contrib + command: + - --config=/etc/otelcol/config.yaml + ports: + - 4317:4317 + - 4318:4318 + volumes: + - ./docker/otelcol/config.yaml:/etc/otelcol/config.yaml diff --git a/docker/otelcol/config.yaml b/docker/otelcol/config.yaml index f53fcc5..a364324 100644 --- a/docker/otelcol/config.yaml +++ b/docker/otelcol/config.yaml @@ -1,19 +1,17 @@ receivers: otlp: protocols: - http: 0.0.0.0:4138 - grpc: 0.0.0.0:4137 - -processors: - batch: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 exporters: - logging: - loglevel: debug + debug: + verbosity: detailed service: pipelines: logs: receivers: [otlp] - processors: [batch] - exporters: [logging] + exporters: [debug] diff --git a/go.mod b/go.mod index 79ed547..e99e210 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,12 @@ module gitea.konchin.com/ytshih/inp2025/game go 1.24.5 require ( + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.7 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/go-resty/resty/v2 v2.16.5 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.20.1 github.com/swaggo/http-swagger v1.3.4 @@ -12,9 +18,12 @@ require ( github.com/uptrace/bun/driver/sqliteshim v1.2.15 github.com/uptrace/bun/extra/bunotel v1.2.15 github.com/uptrace/bunrouter v1.0.23 - github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 + github.com/vmihailenco/msgpack/v5 v5.4.1 + go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 + go.opentelemetry.io/otel/sdk/log v0.14.0 go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/zap v1.27.0 ) @@ -23,11 +32,8 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/charmbracelet/bubbles v0.21.0 // indirect - github.com/charmbracelet/bubbletea v1.3.7 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/charmbracelet/colorprofile v0.3.2 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect @@ -41,10 +47,8 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.3 // indirect - github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -71,34 +75,23 @@ require ( github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect - github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect - github.com/uptrace/uptrace-go v1.37.0 // indirect - github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 // indirect - go.opentelemetry.io/otel/log v0.13.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/net v0.42.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.35.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/grpc v1.73.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 50fca37..1218ab6 100644 --- a/go.sum +++ b/go.sum @@ -4,22 +4,18 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.7 h1:FNaEEFEenOEPnZsY9MI64thl2c84MI66+1QaQbxGOl4= github.com/charmbracelet/bubbletea v1.3.7/go.mod h1:PEOcbQCNzJ2BYUd484kHPO5g3kLO28IffOdFeI2EWus= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= @@ -61,14 +57,18 @@ github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptd github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -167,12 +167,6 @@ github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= -github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= -github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= -github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 h1:cj/Z6FKTTYBnstI0Lni9PA+k2foounKIPUmj1LBwNiQ= -github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2/go.mod h1:LDaXk90gKEC2nC7JH3Lpnhfu+2V7o/TsqomJJmqA39o= -github.com/uptrace/uptrace-go v1.37.0 h1:9ohbWB0qZEfcPLFbfqAAt5wz2rcBmL60/QqkOkvqYOs= -github.com/uptrace/uptrace-go v1.37.0/go.mod h1:3xAdXLVyEoqvRwuj3D/n1s9bLl7Ok+OnNaW889fvtDQ= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -181,38 +175,32 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 h1:ZIt0ya9/y4WyRIzfLC8hQRRsWg0J9M9GyaGtIMiElZI= -go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0/go.mod h1:F1aJ9VuiKWOlWwKdTYDUp1aoS0HzQxg38/VLxKmhm5U= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 h1:zUfYw8cscHHLwaY8Xz3fiJu+R59xBnkgq2Zr1lwmK/0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:514JLMCcFLQFS8cnTepOk6I09cKWJ5nGHBxHrMJ8Yfg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 h1:9PgnL3QNlj10uGxExowIDIZu66aVBwWhXmbOp1pa6RA= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0/go.mod h1:0ineDcLELf6JmKfuo0wvvhAVMuxWFYvkTin2iV4ydPQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4= -go.opentelemetry.io/otel/log v0.6.0 h1:nH66tr+dmEgW5y+F9LanGJUBYPrRgP4g2EkmPE3LeK8= -go.opentelemetry.io/otel/log v0.6.0/go.mod h1:KdySypjQHhP069JX0z/t26VHwa8vSwzgaKmXtIB3fJM= -go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls= -go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A= +go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ= -go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= +go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -224,36 +212,35 @@ golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go.work.sum b/go.work.sum index 37cfe5a..045719f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,6 +1,7 @@ cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= @@ -9,6 +10,7 @@ cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyT cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= @@ -18,6 +20,7 @@ cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5Pi github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= @@ -42,6 +45,7 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= @@ -55,10 +59,11 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2T github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= @@ -109,6 +114,7 @@ go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/detectors/gcp v1.29.0 h1:TiaiXB4DpGD3sdzNlYQxruQngn5Apwzi1X0DRhuGvDQ= go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= @@ -119,9 +125,11 @@ go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXf go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= @@ -131,13 +139,13 @@ golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsO golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0= google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= diff --git a/implements/bunDatabase.go b/implements/bunDatabase.go index 4132e50..22e6c54 100644 --- a/implements/bunDatabase.go +++ b/implements/bunDatabase.go @@ -38,16 +38,6 @@ func (self *BunDatabase) GetUserStatuses( return ret, err } -func (self *BunDatabase) GetRooms( - ctx context.Context, -) ([]models.Room, error) { - var ret []models.Room - err := self.db.NewSelect(). - Model(&ret). - Scan(ctx) - return ret, err -} - func (self *BunDatabase) InsertUser( ctx context.Context, user models.User, @@ -81,16 +71,6 @@ func (self *BunDatabase) InsertUserStatus( return err } -func (self *BunDatabase) InsertRoom( - ctx context.Context, - room models.Room, -) error { - _, err := self.db.NewInsert(). - Model(&room). - Exec(ctx) - return err -} - func (self *BunDatabase) DeleteUserStatus( ctx context.Context, username string, @@ -101,14 +81,3 @@ func (self *BunDatabase) DeleteUserStatus( Exec(ctx) return err } - -func (self *BunDatabase) DeleteRoom( - ctx context.Context, - roomId int, -) error { - _, err := self.db.NewDelete(). - Model(&models.Room{Id: roomId}). - WherePK(). - Exec(ctx) - return err -} diff --git a/interfaces/database.go b/interfaces/database.go index 5576fc1..93178c5 100644 --- a/interfaces/database.go +++ b/interfaces/database.go @@ -16,10 +16,6 @@ type Database interface { ctx context.Context, ) ([]models.UserStatus, error) - GetRooms( - ctx context.Context, - ) ([]models.Room, error) - InsertUser( ctx context.Context, user models.User, @@ -30,18 +26,8 @@ type Database interface { userStatus models.UserStatus, ) error - InsertRoom( - ctx context.Context, - room models.Room, - ) error - DeleteUserStatus( ctx context.Context, username string, ) error - - DeleteRoom( - ctx context.Context, - roomId int, - ) error } diff --git a/models/room.go b/models/room.go deleted file mode 100644 index b96683f..0000000 --- a/models/room.go +++ /dev/null @@ -1,47 +0,0 @@ -package models - -import ( - "fmt" - - "github.com/uptrace/bun" -) - -type RoomType int - -const ( - RoomTypeWordle RoomType = iota -) - -var ( - RoomTypeStr = []string{"Wordle"} -) - -type RoomStatus int - -const ( - RoomStatusWaiting RoomStatus = iota - RoomStatusPlaying - RoomStatusEnded -) - -var ( - RoomStatusStr = []string{"Waiting", "Playing", "Ended"} -) - -type Room struct { - bun.BaseModel `bun:"table:room"` - - Id int `bun:"id,pk,autoincrement"` - Creater string `bun:"creater"` - Type RoomType `bun:"type"` - Status RoomStatus `bun:"status"` -} - -func (self Room) View() string { - return fmt.Sprintf("%2d. %10s %10s [%s]", - self.Id, - self.Creater, - RoomTypeStr[self.Type], - RoomStatusStr[self.Status], - ) -} diff --git a/plays/base.go b/plays/base.go index 228eeda..fb7c7e4 100644 --- a/plays/base.go +++ b/plays/base.go @@ -20,12 +20,14 @@ func Tick(d time.Duration) tea.Cmd { } type Base struct { - client *resty.Client + client *resty.Client + username string } func NewBase(client *resty.Client) *Base { return &Base{ - client: client, + client: client, + username: "", } } diff --git a/plays/game.go b/plays/game.go new file mode 100644 index 0000000..43229e7 --- /dev/null +++ b/plays/game.go @@ -0,0 +1,128 @@ +package plays + +import ( + "fmt" + "net/url" + "sync" + + "gitea.konchin.com/ytshih/inp2025/game/server/wordle" + "gitea.konchin.com/ytshih/inp2025/game/tracing" + "gitea.konchin.com/ytshih/inp2025/game/types" + tea "github.com/charmbracelet/bubbletea" + "github.com/gorilla/websocket" + "github.com/vmihailenco/msgpack/v5" + "go.uber.org/zap" +) + +type Game struct { + *Base + server *url.URL + roomID string + + mutex sync.RWMutex + state types.WordleState + + input string +} + +func (m *Game) GetState() { + urlStr := m.server.String() + tracing.Logger.Info("dialing websocket", zap.String("url", urlStr)) + c, _, err := websocket.DefaultDialer.Dial(urlStr, m.Base.client.Header) + if err != nil { + panic(fmt.Errorf("failed to dial, %w", err)) + } + defer c.Close() + + for { + _, b, err := c.ReadMessage() + switch err.(type) { + case nil: + var state types.WordleState + if err := msgpack.Unmarshal(b, &state); err != nil { + panic(fmt.Errorf("failed to unmarshal state, %w", err)) + } + + m.mutex.Lock() + m.state = state + m.mutex.Unlock() + case *websocket.CloseError: + return + default: + panic(fmt.Errorf("failed to read message, %w", err)) + } + } +} + +func NewGame(base *Base, server *url.URL, roomID string) *Game { + m := &Game{ + Base: base, + server: server, + roomID: roomID, + state: types.NewWordleState(), + input: "", + } + + go m.GetState() + return m +} + +func (m *Game) Init() tea.Cmd { + return tea.Batch(tea.ClearScreen) +} + +func (m *Game) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if _, cmd := m.Base.Update(msg); cmd != nil { + return m, cmd + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyBackspace: + if len(m.input) > 0 { + m.input = m.input[:len(m.input)-1] + } + case tea.KeyEnter: + if len(m.input) == types.MaxLength { + return m, tea.Quit + } + case tea.KeyRunes: + if len(m.input) < types.MaxLength { + m.input = m.input + string(msg.Runes) + } + } + } + + return m, nil +} + +func (m *Game) View() string { + m.mutex.RLock() + ret := m.state.View() + m.mutex.RUnlock() + return ret +} + +func (m *Game) Next(queue *[]*tea.Program) error { + if len(m.input) == types.MaxLength { + b, err := msgpack.Marshal(wordle.Operation{ + Type: wordle.OperationTypeGuess, + Username: m.username, + Guess: m.input, + }) + if err != nil { + return err + } + _, err = m.Base.client.R(). + SetBody(b). + Post(fmt.Sprintf("/api/rooms/%s/guess", m.roomID)) + if err != nil { + return err + } + + *queue = append(*queue, + tea.NewProgram(NewGame(m.Base, m.server, m.roomID))) + } + return nil +} \ No newline at end of file diff --git a/plays/joinRoom.go b/plays/joinRoom.go new file mode 100644 index 0000000..427bde9 --- /dev/null +++ b/plays/joinRoom.go @@ -0,0 +1,89 @@ +package plays + +import ( + "fmt" + "net/http" + "strings" + + "gitea.konchin.com/ytshih/inp2025/game/types" + ea "github.com/charmbracelet/bubbletea" +) + +type JoinRoom struct { + *Base + rooms []types.Room + cursor int + choice *types.Room +} + +func NewJoinRoom(base *Base, rooms []types.Room) *JoinRoom { + return &JoinRoom{ + Base: base, + rooms: rooms, + } +} + +func (m *JoinRoom) Init() ea.Cmd { + return ea.ClearScreen +} + +func (m *JoinRoom) Update(msg ea.Msg) (ea.Model, ea.Cmd) { + switch msg := msg.(type) { + case ea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, ea.Quit + case "enter": + if m.cursor < len(m.rooms) { + m.choice = &m.rooms[m.cursor] + } + return m, ea.Quit + case "up": + if m.cursor > 0 { + m.cursor-- + } + case "down": + if m.cursor < len(m.rooms)-1 { + m.cursor++ + } + } + } + return m, nil +} + +func (m *JoinRoom) View() string { + var b strings.Builder + b.WriteString("Select a room to join:\n\n") + + for i, room := range m.rooms { + if m.cursor == i { + b.WriteString("(•) ") + } else { + b.WriteString("( ) ") + } + b.WriteString(room.View() + "\n") + } + + return b.String() +} + +func (m *JoinRoom) Next(queue *[]*ea.Program) error { + if m.choice == nil { + *queue = append(*queue, ea.NewProgram(NewLobby(m.Base))) + return nil + } + + resp, err := m.Base.client.R(). + SetResult(&types.Room{}). + Post(fmt.Sprintf("/api/rooms/%s/join", m.choice.ID)) + + if err != nil || resp.StatusCode() != http.StatusOK { + *queue = append(*queue, ea.NewProgram(NewRedirect("Failed to join room"))) + *queue = append(*queue, ea.NewProgram(NewLobby(m.Base))) + return nil + } + + room := resp.Result().(*types.Room) + *queue = append(*queue, ea.NewProgram(NewRoomWaiting(m.Base, room.ID))) + return nil +} diff --git a/plays/lobby.go b/plays/lobby.go index 5a9367e..c1e7da5 100644 --- a/plays/lobby.go +++ b/plays/lobby.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "strings" - "time" "gitea.konchin.com/ytshih/inp2025/game/models" "gitea.konchin.com/ytshih/inp2025/game/tracing" @@ -14,7 +13,7 @@ import ( ) var ( - lobbyChoices = []string{"No-op", "Logout"} + lobbyChoices = []string{"Create Room", "Join Room", "Refresh", "Logout"} ) type Lobby struct { @@ -23,62 +22,44 @@ type Lobby struct { choice string cursor int - updateCh chan struct{} - users []models.UserStatus - rooms []models.Room + users []models.UserStatus + rooms []types.Room } func NewLobby(base *Base) *Lobby { - m := Lobby{ - Base: base, - choice: "", - cursor: 0, - updateCh: make(chan struct{}, 1), + m := &Lobby{ + Base: base, + choice: "", + cursor: 0, } - - return &m + return m } -func updateLobbyInfo(m *Lobby) error { - for { - select { - case <-m.updateCh: - return nil - default: - var users []models.UserStatus - resp, err := m.Base.client.R(). - SetResult(&users). - ForceContentType("application/json"). - Get("/api/lobby/users") - if resp.StatusCode() != http.StatusOK { - tracing.Logger. - Error("failed to get lobby users", - zap.Error(err)) - return err - } - m.users = users - - var rooms []models.Room - _, err = m.Base.client.R(). - SetResult(&rooms). - ForceContentType("application/json"). - Get("/api/lobby/rooms") - if err != nil { - tracing.Logger. - Error("failed to get lobby rooms", - zap.Error(err)) - return err - } - m.rooms = rooms - - time.Sleep(refetchTick) - } +func (m *Lobby) fetchLobbyInfo() tea.Msg { + var users []models.UserStatus + resp, err := m.Base.client.R(). + SetResult(&users). + Get("/api/lobby/users") + if err != nil || resp.StatusCode() != http.StatusOK { + tracing.Logger.Error("failed to get lobby users", zap.Error(err)) + return nil } + m.users = users + + var rooms []types.Room + resp, err = m.Base.client.R(). + SetResult(&rooms). + Get("/api/lobby/rooms") + if err != nil || resp.StatusCode() != http.StatusOK { + tracing.Logger.Error("failed to get lobby rooms", zap.Error(err)) + return nil + } + m.rooms = rooms + return nil } func (m *Lobby) Init() tea.Cmd { - go updateLobbyInfo(m) - return tea.Sequence(tea.ClearScreen, Tick(refreshTick)) + return tea.Batch(tea.ClearScreen, m.fetchLobbyInfo) } func (m *Lobby) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -107,8 +88,6 @@ func (m *Lobby) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cursor = len(lobbyChoices) - 1 } } - case types.TickMsg: - return m, Tick(refreshTick) } return m, nil } @@ -116,19 +95,20 @@ func (m *Lobby) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *Lobby) View() string { var b strings.Builder - fmt.Fprintf(&b, "Game lobby\n") + fmt.Fprintf(&b, "Game lobby\n\n") - fmt.Fprintf(&b, "User Status (%d):\n", len(m.users)) + fmt.Fprintf(&b, "Online Users (%d):\n", len(m.users)) for _, user := range m.users { b.WriteString("- " + user.View() + "\n") } + b.WriteString("\n") - fmt.Fprintf(&b, "Room Status (%d):\n", len(m.rooms)) + fmt.Fprintf(&b, "Game Rooms (%d):\n", len(m.rooms)) for _, room := range m.rooms { b.WriteString("- " + room.View() + "\n") } - b.WriteString("==========\n") + b.WriteString("\n==========\n") for i := 0; i < len(lobbyChoices); i++ { if m.cursor == i { @@ -142,11 +122,23 @@ func (m *Lobby) View() string { } func (m *Lobby) Next(queue *[]*tea.Program) error { - m.updateCh <- struct{}{} switch m.choice { - case "No-op": + case "Refresh": *queue = append(*queue, tea.NewProgram(NewLobby(m.Base))) + case "Create Room": + resp, err := m.Base.client.R(). + SetResult(&types.Room{}). + Post("/api/rooms") + if err != nil || resp.StatusCode() != http.StatusOK { + return fmt.Errorf("failed to create room") + } + room := resp.Result().(*types.Room) + *queue = append(*queue, + tea.NewProgram(NewRoomWaiting(m.Base, room.ID))) + case "Join Room": + *queue = append(*queue, + tea.NewProgram(NewJoinRoom(m.Base, m.rooms))) case "Logout": *queue = append(*queue, tea.NewProgram(NewLogout(m.Base))) diff --git a/plays/login.go b/plays/login.go index 309522a..ec4e2ea 100644 --- a/plays/login.go +++ b/plays/login.go @@ -125,6 +125,7 @@ func (m *Login) Next(queue *[]*tea.Program) error { if resp.StatusCode() == http.StatusOK { m.Base.client. SetBasicAuth(username, password) + m.Base.username = username *queue = append(*queue, tea.NewProgram(NewLobby(m.Base))) } else { diff --git a/plays/register.go b/plays/register.go index 85ab1b9..a26ccbb 100644 --- a/plays/register.go +++ b/plays/register.go @@ -123,17 +123,19 @@ func (m *Register) Next(queue *[]*tea.Program) error { }). Post("/auth/register") - switch resp.StatusCode() { - case http.StatusOK: + if err == nil && resp.StatusCode() == http.StatusOK { *queue = append(*queue, tea.NewProgram(NewLogin(m.Base))) - case http.StatusBadRequest: - *queue = append(*queue, - tea.NewProgram(NewRedirect("Username already exist"))) - *queue = append(*queue, - tea.NewProgram(NewRegister(m.Base))) - case http.StatusInternalServerError: - return err + } else { + switch resp.StatusCode() { + case http.StatusBadRequest: + *queue = append(*queue, + tea.NewProgram(NewRedirect("Username already exist"))) + *queue = append(*queue, + tea.NewProgram(NewRegister(m.Base))) + default: + return err + } } return nil diff --git a/plays/roomWaiting.go b/plays/roomWaiting.go new file mode 100644 index 0000000..2d3ed40 --- /dev/null +++ b/plays/roomWaiting.go @@ -0,0 +1,90 @@ +package plays + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "gitea.konchin.com/ytshih/inp2025/game/tracing" + "gitea.konchin.com/ytshih/inp2025/game/types" + tea "github.com/charmbracelet/bubbletea" + "go.uber.org/zap" +) + +type RoomWaiting struct { + *Base + roomID string + room *types.Room +} + +func NewRoomWaiting(base *Base, roomID string) *RoomWaiting { + return &RoomWaiting{ + Base: base, + roomID: roomID, + } +} + +func (m *RoomWaiting) checkRoomStatus() tea.Msg { + resp, err := m.Base.client.R(). + SetResult(&types.Room{}). + Get(fmt.Sprintf("/api/rooms/%s", m.roomID)) + + if err != nil || resp.StatusCode() != http.StatusOK { + return types.TickMsg{} // Keep polling + } + + room := resp.Result().(*types.Room) + if room.Status == types.RoomStatusPlaying { + m.room = room + return tea.Quit() // Stop polling and proceed + } + + return types.TickMsg{} // Keep polling +} + +func (m *RoomWaiting) Init() tea.Cmd { + return tea.Batch(tea.ClearScreen, func() tea.Msg { + return types.TickMsg(time.Now()) + }) +} + +func (m *RoomWaiting) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case types.TickMsg: + time.Sleep(1 * time.Second) + return m, m.checkRoomStatus + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + } + } + return m, nil +} + +func (m *RoomWaiting) View() string { + var b strings.Builder + b.WriteString("Waiting for another player to join...\n") + b.WriteString(fmt.Sprintf("Room ID: %s\n", m.roomID)) + return b.String() +} + +func (m *RoomWaiting) Next(queue *[]*tea.Program) error { + if m.room != nil && m.room.Status == types.RoomStatusPlaying { + hostURL := m.Base.client.HostURL + wsHost := strings.Replace(hostURL, "http", "ws", 1) + wsURL := fmt.Sprintf("%s/api/rooms/%s/state", wsHost, m.roomID) + tracing.Logger.Info("parsing websocket url", zap.String("url", wsURL)) + u, err := url.Parse(wsURL) + if err != nil { + return err + } + *queue = append(*queue, tea.NewProgram(NewGame(m.Base, u, m.roomID))) + } else { + // If we quit without the room being ready, go back to the lobby. + *queue = append(*queue, tea.NewProgram(NewLobby(m.Base))) + } + return nil +} \ No newline at end of file diff --git a/backend/handlers/api/getLobbyUsers.go b/server/backend/api/getLobbyUsers.go similarity index 89% rename from backend/handlers/api/getLobbyUsers.go rename to server/backend/api/getLobbyUsers.go index 7824076..e75c1a7 100644 --- a/backend/handlers/api/getLobbyUsers.go +++ b/server/backend/api/getLobbyUsers.go @@ -3,7 +3,7 @@ package api import ( "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "github.com/uptrace/bunrouter" ) diff --git a/backend/handlers/api/getUser.go b/server/backend/api/getUser.go similarity index 94% rename from backend/handlers/api/getUser.go rename to server/backend/api/getUser.go index 5dfa98d..1e8bd99 100644 --- a/backend/handlers/api/getUser.go +++ b/server/backend/api/getUser.go @@ -4,7 +4,7 @@ import ( "database/sql" "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" "github.com/uptrace/bunrouter" ) diff --git a/server/backend/api/handlers.go b/server/backend/api/handlers.go new file mode 100644 index 0000000..2e8a98d --- /dev/null +++ b/server/backend/api/handlers.go @@ -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} +} diff --git a/server/backend/api/rooms.go b/server/backend/api/rooms.go new file mode 100644 index 0000000..98a1bd0 --- /dev/null +++ b/server/backend/api/rooms.go @@ -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) +} diff --git a/backend/handlers/auth/getLogin.go b/server/backend/auth/getLogin.go similarity index 92% rename from backend/handlers/auth/getLogin.go rename to server/backend/auth/getLogin.go index 425db1c..65d146f 100644 --- a/backend/handlers/auth/getLogin.go +++ b/server/backend/auth/getLogin.go @@ -3,8 +3,8 @@ package auth import ( "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "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" diff --git a/backend/handlers/auth/handlers.go b/server/backend/auth/handlers.go similarity index 100% rename from backend/handlers/auth/handlers.go rename to server/backend/auth/handlers.go diff --git a/backend/handlers/auth/postLogout.go b/server/backend/auth/postLogout.go similarity index 93% rename from backend/handlers/auth/postLogout.go rename to server/backend/auth/postLogout.go index ed0d3e0..42f22ca 100644 --- a/backend/handlers/auth/postLogout.go +++ b/server/backend/auth/postLogout.go @@ -3,8 +3,8 @@ package auth import ( "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "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" diff --git a/backend/handlers/auth/postRegister.go b/server/backend/auth/postRegister.go similarity index 95% rename from backend/handlers/auth/postRegister.go rename to server/backend/auth/postRegister.go index 7206fca..b83cd6a 100644 --- a/backend/handlers/auth/postRegister.go +++ b/server/backend/auth/postRegister.go @@ -5,8 +5,8 @@ import ( "io" "net/http" - "gitea.konchin.com/ytshih/inp2025/game/backend/middlewares" "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" diff --git a/backend/docs/docs.go b/server/docs/docs.go similarity index 96% rename from backend/docs/docs.go rename to server/docs/docs.go index f546d4f..d85611e 100644 --- a/backend/docs/docs.go +++ b/server/docs/docs.go @@ -18,11 +18,6 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/lobby/rooms": { - "get": { - "responses": {} - } - }, "/api/lobby/users": { "get": { "responses": {} diff --git a/backend/docs/swagger.json b/server/docs/swagger.json similarity index 95% rename from backend/docs/swagger.json rename to server/docs/swagger.json index 11669d1..5f85f63 100644 --- a/backend/docs/swagger.json +++ b/server/docs/swagger.json @@ -10,11 +10,6 @@ }, "basePath": "/", "paths": { - "/api/lobby/rooms": { - "get": { - "responses": {} - } - }, "/api/lobby/users": { "get": { "responses": {} diff --git a/backend/docs/swagger.yaml b/server/docs/swagger.yaml similarity index 95% rename from backend/docs/swagger.yaml rename to server/docs/swagger.yaml index e7815b6..6af4478 100644 --- a/backend/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -14,9 +14,6 @@ info: title: Intro. to Network Programming Game version: 0.0.1-alpha paths: - /api/lobby/rooms: - get: - responses: {} /api/lobby/users: get: responses: {} diff --git a/backend/middlewares/accessLog.go b/server/middlewares/accessLog.go similarity index 90% rename from backend/middlewares/accessLog.go rename to server/middlewares/accessLog.go index 7243584..68498ad 100644 --- a/backend/middlewares/accessLog.go +++ b/server/middlewares/accessLog.go @@ -11,7 +11,7 @@ func (self *Handlers) AccessLog( next bunrouter.HandlerFunc, ) bunrouter.HandlerFunc { return func(w http.ResponseWriter, req bunrouter.Request) error { - tracing.Logger.Ctx(req.Context()). + tracing.Logger. Info(req.Method + " " + req.Params().Route()) return next(w, req) } diff --git a/backend/middlewares/auth.go b/server/middlewares/auth.go similarity index 97% rename from backend/middlewares/auth.go rename to server/middlewares/auth.go index 86af671..fcbbc39 100644 --- a/backend/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -35,7 +35,7 @@ func (self *Handlers) Auth( } if dbUser.Password != password { - tracing.Logger.Ctx(ctx). + tracing.Logger. Debug("password input", zap.String("input.password", password), zap.String("dbuser.password", dbUser.Password)) diff --git a/backend/middlewares/bunrouterOtel.go b/server/middlewares/bunrouterOtel.go similarity index 100% rename from backend/middlewares/bunrouterOtel.go rename to server/middlewares/bunrouterOtel.go diff --git a/backend/middlewares/errorHandler.go b/server/middlewares/errorHandler.go similarity index 92% rename from backend/middlewares/errorHandler.go rename to server/middlewares/errorHandler.go index 5fb99f5..87a42f1 100644 --- a/backend/middlewares/errorHandler.go +++ b/server/middlewares/errorHandler.go @@ -45,7 +45,7 @@ func (self *Handlers) ErrorHandler( httpErr = err default: - tracing.Logger.Ctx(ctx). + tracing.Logger. Error("unhandled error", zap.Error(err)) httpErr = NewHTTPError(err) @@ -60,20 +60,20 @@ func (self *Handlers) ErrorHandler( if 500 <= httpErr.StatusCode && httpErr.StatusCode <= 599 { span.SetStatus(codes.Error, err.Error()) if httpErr.OriginError == nil { - tracing.Logger.Ctx(ctx). + tracing.Logger. Error(httpErr.Message) } else { - tracing.Logger.Ctx(ctx). + tracing.Logger. Error(httpErr.Message, zap.Error(httpErr.OriginError)) } } else { span.SetStatus(codes.Ok, "") if httpErr.OriginError == nil { - tracing.Logger.Ctx(ctx). + tracing.Logger. Warn(httpErr.Message) } else { - tracing.Logger.Ctx(ctx). + tracing.Logger. Warn(httpErr.Message, zap.Error(httpErr.OriginError)) } diff --git a/backend/middlewares/handler.go b/server/middlewares/handler.go similarity index 100% rename from backend/middlewares/handler.go rename to server/middlewares/handler.go diff --git a/server/rooms/manager.go b/server/rooms/manager.go new file mode 100644 index 0000000..a379d92 --- /dev/null +++ b/server/rooms/manager.go @@ -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 +} diff --git a/server/wordle/handlers.go b/server/wordle/handlers.go new file mode 100644 index 0000000..61c4eff --- /dev/null +++ b/server/wordle/handlers.go @@ -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 + } + } + } + } +} \ No newline at end of file diff --git a/server/wordle/wsGetState.go b/server/wordle/wsGetState.go new file mode 100644 index 0000000..60e7dea --- /dev/null +++ b/server/wordle/wsGetState.go @@ -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) +} diff --git a/server/wordle/wsPostGuess.go b/server/wordle/wsPostGuess.go new file mode 100644 index 0000000..7d19428 --- /dev/null +++ b/server/wordle/wsPostGuess.go @@ -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) +} diff --git a/tracing/tracer.go b/tracing/tracer.go index d94d910..9af7d93 100644 --- a/tracing/tracer.go +++ b/tracing/tracer.go @@ -1,33 +1,42 @@ package tracing import ( + "context" + "github.com/spf13/viper" - "github.com/uptrace/opentelemetry-go-extra/otelzap" - "go.opentelemetry.io/otel" + "go.opentelemetry.io/contrib/bridges/otelzap" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) var ( - Tracer trace.Tracer - Logger *otelzap.Logger + Provider *log.LoggerProvider + Tracer trace.Tracer + Logger *zap.Logger version string = "0.0.1-alpha" ) func InitTracer(appname string) { - Tracer = otel.Tracer(appname) - - var l *zap.Logger - var err error - if viper.GetBool("zap-production") { - l, err = zap.NewProduction() - } else { - l, err = zap.NewDevelopment() - } + ctx := context.Background() + exp, err := otlploggrpc.New(ctx, + otlploggrpc.WithInsecure(), + otlploggrpc.WithEndpoint(viper.GetString("otel-endpoint"))) if err != nil { panic(err) } - Logger = otelzap.New(l) + Provider = log.NewLoggerProvider( + log.WithProcessor(log.NewBatchProcessor(exp))) + otelCore := otelzap.NewCore(appname, + otelzap.WithLoggerProvider(Provider)) + + Logger = zap.New(otelCore, zap.AddCaller()) +} + +func DeferTracer() { + Logger.Sync() + Provider.Shutdown(context.Background()) } diff --git a/types/game.go b/types/game.go new file mode 100644 index 0000000..4194579 --- /dev/null +++ b/types/game.go @@ -0,0 +1,31 @@ +package types + +import "fmt" + +type RoomStatus int + +const ( + RoomStatusWaiting RoomStatus = iota + RoomStatusPlaying + RoomStatusEnded +) + +var ( + RoomStatusStr = []string{"Waiting", "Playing", "Ended"} +) + +type Room struct { + ID string `json:"id"` + Creater string `json:"creater"` + Status RoomStatus `json:"status"` + Players []string `json:"players"` + Addr string `json:"-"` +} + +func (self Room) View() string { + return fmt.Sprintf("%s's room [%s] (%d/2)", + self.Creater, + RoomStatusStr[self.Status], + len(self.Players), + ) +} diff --git a/types/wordle.go b/types/wordle.go new file mode 100644 index 0000000..3178522 --- /dev/null +++ b/types/wordle.go @@ -0,0 +1,89 @@ +package types + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +type CharStatus int + +const ( + CharStatusA CharStatus = iota + CharStatusB + CharStatusC +) + +type Char struct { + Char rune `msgpack:"char"` + Status CharStatus `msgpack:"status"` +} + +func NewChar() Char { + return Char{ + Char: 0x0, + Status: CharStatusC, + } +} + +func (self Char) View() string { + style := lipgloss.NewStyle() + + switch self.Status { + case CharStatusA: + style = style.Background(lipgloss.Color("#00ff00")) + case CharStatusB: + style = style.Background(lipgloss.Color("#ffff00")) + case CharStatusC: + style = style.Background(lipgloss.Color("#808080")) + } + + return style.Render(string(self.Char)) +} + +type Word struct { + Chars []Char `msgpack:"chars"` +} + +func NewWord(word, ans string) Word { + return Word{ + Chars: nil, + } +} + +func (self Word) View() string { + var b strings.Builder + + for _, char := range self.Chars { + b.WriteString(char.View()) + b.WriteRune('\n') + } + + return b.String() +} + +const ( + MaxLength = 5 +) + +type UserState struct { + History []Word `msgpack:"history"` + Input string +} + +type WordleState struct { + Users map[string]UserState `msgpack:"users"` + CurrentGuess int +} + +func NewWordleState() WordleState { + return WordleState{ + Users: nil, + } +} + +func (self WordleState) View() string { + var b strings.Builder + + return b.String() +} diff --git a/utils/initDB.go b/utils/initDB.go index 3bf9609..a0199fc 100644 --- a/utils/initDB.go +++ b/utils/initDB.go @@ -11,6 +11,5 @@ func InitDB(ctx context.Context, db *bun.DB) error { return db.ResetModel(ctx, (*models.User)(nil), (*models.UserStatus)(nil), - (*models.Room)(nil), ) } diff --git a/utils/readConfig.go b/utils/readConfig.go index 179cf0e..1f09a6d 100644 --- a/utils/readConfig.go +++ b/utils/readConfig.go @@ -14,7 +14,7 @@ func ReadConfig(ctx context.Context) { viper.AddConfigPath(".") if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - tracing.Logger.Ctx(ctx). + tracing.Logger. Panic("failed to read config file", zap.Error(err)) panic(err) diff --git a/utils/udp.go b/utils/udp.go new file mode 100644 index 0000000..5074071 --- /dev/null +++ b/utils/udp.go @@ -0,0 +1,77 @@ +package utils + +import ( + "fmt" + "net" + + "github.com/vmihailenco/msgpack/v5" +) + +type UDPPayload struct { + MagicNumber int `msgpack:"magicNumber"` + Data string `msgpack:"data"` +} + +const ( + BUFFER_SIZE int = 1024 + MAGIC_NUMBER int = 114514 +) + +func ListenUDP( + port string, + doneCh chan struct{}, +) (string, error) { + addr, err := net.ResolveUDPAddr("udp", ":"+port) + if err != nil { + return "", fmt.Errorf("failed to resolve address, %w", err) + } + + conn, err := net.ListenUDP("udp", addr) + if err != nil { + return "", fmt.Errorf("failed to listen on addr '%v', %w", addr, err) + } + defer conn.Close() + + dataCh := make(chan string) + go func() { + for { + buffer := make([]byte, BUFFER_SIZE) + + n, _, err := conn.ReadFromUDP(buffer) + if err != nil { + continue + } + + var payload UDPPayload + err = msgpack.Unmarshal(buffer[:n], &payload) + if err == nil && payload.MagicNumber == MAGIC_NUMBER { + dataCh <- payload.Data + return + } + } + }() + + select { + case <-doneCh: + return "", nil + case data := <-dataCh: + return data, nil + } + + return "", fmt.Errorf("failed to receive data") +} + +func SendPayload(endpoint, data string) error { + conn, err := net.Dial("udp", endpoint) + if err != nil { + return fmt.Errorf("failed to dial endpoint") + } + defer conn.Close() + + _, err = conn.Write([]byte(data)) + if err != nil { + return fmt.Errorf("failed to send payload") + } + + return nil +} diff --git a/workflows/wordleServer.go b/workflows/wordleServer.go new file mode 100644 index 0000000..8a284c4 --- /dev/null +++ b/workflows/wordleServer.go @@ -0,0 +1,47 @@ +package workflows + +import ( + "context" + "net/http" + + "gitea.konchin.com/ytshih/inp2025/game/implements" + "gitea.konchin.com/ytshih/inp2025/game/server/middlewares" + "gitea.konchin.com/ytshih/inp2025/game/server/wordle" + "gitea.konchin.com/ytshih/inp2025/game/tracing" + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +type ShutdownFunc = func() + +func WordleServer(addr string) ShutdownFunc { + handlers := wordle.NewHandlers() + middlewareHandlers := middlewares.NewHandlers( + implements.NewBunDatabase(nil)) + + router := bunrouter.New() + + apiGroup := router.NewGroup("/api"). + Use(middlewareHandlers.ErrorHandler) + apiGroup.GET("/state", + handlers.WSGetState) + apiGroup.POST("/guess", + handlers.WSPostGuess) + + server := &http.Server{ + Addr: addr, + Handler: http.Handler(router), + } + + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + tracing.Logger.Error("wordle server failed", zap.Error(err)) + } + }() + + return func() { + if err := server.Shutdown(context.Background()); err != nil { + tracing.Logger.Error("failed to shutdown wordle server", zap.Error(err)) + } + } +}