Feat: works on my machine
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
game
|
||||
logs/
|
||||
14
Makefile
Normal file
14
Makefile
Normal file
@@ -0,0 +1,14 @@
|
||||
.PHONY: all clean
|
||||
|
||||
GO_ARGS += CGO_ENABLED=0
|
||||
|
||||
SOURCE := $(shell find . -type f -name '*.go')
|
||||
TARGET := game
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SOURCE)
|
||||
$(GO_ARGS) go build -o $@
|
||||
|
||||
clean:
|
||||
-rm -f $(TARGET)
|
||||
11
README.md
Normal file
11
README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Introduction to Network Programming Homework
|
||||
|
||||
## TODO
|
||||
|
||||
- Lobby
|
||||
- Login count
|
||||
- Logout and duplicate login
|
||||
- Connection
|
||||
- Scan port
|
||||
- Game
|
||||
- Game end reason
|
||||
18
auth.go
Normal file
18
auth.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gitea.konchin.com/ytshih/inp2025/workflows"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var authCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
workflows.AuthServer()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
authCmd.Flags().
|
||||
String("listen-addr", "localhost:8888", "")
|
||||
}
|
||||
30
cmd.go
Normal file
30
cmd.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "game",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-"))
|
||||
viper.BindPFlags(cmd.PersistentFlags())
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
RootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.EnableTraverseRunHooks = true
|
||||
|
||||
RootCmd.AddCommand(authCmd)
|
||||
RootCmd.AddCommand(playerCmd)
|
||||
RootCmd.AddCommand(testCmd)
|
||||
}
|
||||
68
go.mod
Normal file
68
go.mod
Normal file
@@ -0,0 +1,68 @@
|
||||
module gitea.konchin.com/ytshih/inp2025
|
||||
|
||||
go 1.25.2
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/uptrace/bun v1.2.15
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.15
|
||||
github.com/uptrace/bunrouter v1.0.23
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // 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-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
)
|
||||
173
go.sum
Normal file
173
go.sum
Normal file
@@ -0,0 +1,173 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
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/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.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
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/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/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.2.15 h1:Ut68XRBLDgp9qG9QBMa9ELWaZOmzHNdczHQdrOZbEFE=
|
||||
github.com/uptrace/bun v1.2.15/go.mod h1:Eghz7NonZMiTX/Z6oKYytJ0oaMEJ/eq3kEV4vSqG038=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 h1:7upGMVjFRB1oI78GQw6ruNLblYn5CR+kxqcbbeBBils=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15/go.mod h1:c7YIDaPNS2CU2uI1p7umFuFWkuKbDcPDDvp+DLHZnkI=
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.15 h1:M/rZJSjOPV4OmfTVnDPtL+wJmdMTqDUn8cuk5ycfABA=
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.15/go.mod h1:YqwxFyvM992XOCpGJtXyKPkgkb+aZpIIMzGbpaw1hIk=
|
||||
github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU=
|
||||
github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M=
|
||||
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=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
|
||||
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
22
go.work.sum
Normal file
22
go.work.sum
Normal file
@@ -0,0 +1,22 @@
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
11
handlers/auth/handlers.go
Normal file
11
handlers/auth/handlers.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package auth
|
||||
|
||||
import "github.com/uptrace/bun"
|
||||
|
||||
type Handlers struct {
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
func NewHandlers(db *bun.DB) *Handlers {
|
||||
return &Handlers{db: db}
|
||||
}
|
||||
59
handlers/auth/postLogin.go
Normal file
59
handlers/auth/postLogin.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
func (self *Handlers) PostLogin(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
user, ok := ctx.Value(types.UserKey).(models.User)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "user not login",
|
||||
}
|
||||
}
|
||||
|
||||
res, err := self.db.NewUpdate().
|
||||
Model((*models.User)(nil)).
|
||||
Set("login_count = login_count + ?", 1).
|
||||
Set("is_logged = ?", true).
|
||||
Where("is_logged = ?", false).
|
||||
Where("username = ?", user.Username).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to update login count",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
if cnt, err := res.RowsAffected(); err != nil || cnt == 0 {
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to get affected row count",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
// debug
|
||||
return utils.Success(w)
|
||||
if cnt == 0 {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "already logged in",
|
||||
}
|
||||
}
|
||||
}
|
||||
return utils.Success(w)
|
||||
}
|
||||
39
handlers/auth/postLogout.go
Normal file
39
handlers/auth/postLogout.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
func (self *Handlers) PostLogout(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
ctx := req.Context()
|
||||
user, ok := ctx.Value(types.UserKey).(models.User)
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "user not login",
|
||||
}
|
||||
}
|
||||
|
||||
_, err := self.db.NewUpdate().
|
||||
Set("is_logged = ?", false).
|
||||
Where("username = ?", user.Username).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to update logged in status",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
66
handlers/auth/postRegister.go
Normal file
66
handlers/auth/postRegister.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
res, err := self.db.NewInsert().
|
||||
Model(&user).
|
||||
On("CONFLICT (username) DO NOTHING").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to insert user",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
if cnt, err := res.RowsAffected(); err != nil || cnt == 0 {
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to get affected row count",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
if cnt == 0 {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "username already exist",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
55
handlers/wordle/getState.go
Normal file
55
handlers/wordle/getState.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package wordle
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
func (self *Handlers) GetState(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
// fmt.Fprintf(os.Stderr, "GET /api/state\n")
|
||||
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()
|
||||
|
||||
username, _, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "username not exist",
|
||||
}
|
||||
}
|
||||
dataCh := make(chan types.WordleState)
|
||||
self.opCh <- &OperationSubs{
|
||||
Username: username,
|
||||
SubsCh: &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)
|
||||
}
|
||||
98
handlers/wordle/handlers.go
Normal file
98
handlers/wordle/handlers.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package wordle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Operation interface {
|
||||
Run(self *Handlers) error
|
||||
}
|
||||
|
||||
type OperationSubs struct {
|
||||
Username string
|
||||
SubsCh *chan types.WordleState
|
||||
}
|
||||
|
||||
func (op *OperationSubs) Run(self *Handlers) error {
|
||||
_, ok := self.state.History[op.Username]
|
||||
if !ok {
|
||||
self.state.History[op.Username] = [types.GUESS_COUNT_LIMIT]types.Guess{}
|
||||
}
|
||||
// fmt.Fprintf(os.Stderr, "[DEBUG] %+v\n", self.state.History)
|
||||
self.subs = append(self.subs, op.SubsCh)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type OperationGuess struct {
|
||||
Username string `msgpack:"username"`
|
||||
Guess string `msgpack:"guess"`
|
||||
}
|
||||
|
||||
func (op *OperationGuess) Run(self *Handlers) error {
|
||||
self.state.CurrentGuess[op.Username] = op.Guess
|
||||
|
||||
if len(self.state.CurrentGuess) < len(self.state.History) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for user, guess := range self.state.CurrentGuess {
|
||||
if guess == self.ans {
|
||||
self.state.GameEnd = true
|
||||
}
|
||||
guesses := self.state.History[user]
|
||||
guesses[self.state.Round] = types.NewGuess(guess, self.ans)
|
||||
self.state.History[user] = guesses
|
||||
}
|
||||
self.state.CurrentGuess = make(map[types.UsernameType]string)
|
||||
self.state.Round++
|
||||
if self.state.Round >= types.GUESS_COUNT_LIMIT {
|
||||
self.state.GameEnd = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Handlers struct {
|
||||
ans string
|
||||
state types.WordleState
|
||||
upgrader websocket.Upgrader
|
||||
subs []*chan types.WordleState
|
||||
|
||||
opCh chan Operation
|
||||
}
|
||||
|
||||
func NewHandlers(ans string) *Handlers {
|
||||
ret := &Handlers{
|
||||
ans: ans,
|
||||
state: types.NewWordleState(),
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
|
||||
opCh: make(chan Operation),
|
||||
}
|
||||
go ret.GameFlow()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (self *Handlers) GameFlow() {
|
||||
for op := range self.opCh {
|
||||
if err := op.Run(self); err != nil {
|
||||
panic(fmt.Errorf("game flow operation failed, %w", err))
|
||||
}
|
||||
|
||||
// fmt.Fprintf(os.Stderr, "[DEBUG] %+v\n", self.state.History)
|
||||
for _, ch := range self.subs {
|
||||
*ch <- self.state
|
||||
}
|
||||
}
|
||||
}
|
||||
54
handlers/wordle/postGuess.go
Normal file
54
handlers/wordle/postGuess.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package wordle
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
type PostGuessInput struct {
|
||||
Guess string `msgpack:"guess"`
|
||||
}
|
||||
|
||||
func (self *Handlers) PostGuess(
|
||||
w http.ResponseWriter,
|
||||
req bunrouter.Request,
|
||||
) error {
|
||||
// fmt.Fprintf(os.Stderr, "POST /api/guess\n")
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "failed to read body payload",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
var input PostGuessInput
|
||||
if err := msgpack.Unmarshal(b, &input); err != nil {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "failed to unmarshal from msgpack",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
|
||||
username, _, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return middlewares.HTTPError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Message: "username not exist",
|
||||
}
|
||||
}
|
||||
|
||||
self.opCh <- &OperationGuess{
|
||||
Username: username,
|
||||
Guess: input.Guess,
|
||||
}
|
||||
|
||||
return utils.Success(w)
|
||||
}
|
||||
60
middlewares/auth.go
Normal file
60
middlewares/auth.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/models"
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"github.com/uptrace/bunrouter"
|
||||
)
|
||||
|
||||
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.StatusNotFound,
|
||||
Message: "username not exist",
|
||||
}
|
||||
}
|
||||
|
||||
dbUser := models.User{Username: username}
|
||||
err := self.db.NewSelect().
|
||||
Model(&dbUser).
|
||||
WherePK().
|
||||
Scan(ctx)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "username not exist",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return HTTPError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Message: "failed to select user from db",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
if password != dbUser.Password {
|
||||
return HTTPError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Message: "password incorrect",
|
||||
OriginError: err,
|
||||
}
|
||||
}
|
||||
return next(w, req.WithContext(context.WithValue(
|
||||
ctx, types.UserKey, models.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})))
|
||||
}
|
||||
}
|
||||
63
middlewares/errorHandler.go
Normal file
63
middlewares/errorHandler.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/uptrace/bunrouter"
|
||||
"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)
|
||||
|
||||
var httpErr HTTPError
|
||||
switch err := err.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
|
||||
case HTTPError:
|
||||
httpErr = err
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unhandled error, %v\n", err)
|
||||
zap.L().Error("unhandled error",
|
||||
zap.Error(err))
|
||||
httpErr = NewHTTPError(err)
|
||||
}
|
||||
|
||||
if httpErr.OriginError == nil {
|
||||
zap.L().Warn(httpErr.Message)
|
||||
} else {
|
||||
zap.L().Warn(httpErr.Message,
|
||||
zap.Error(httpErr.OriginError))
|
||||
}
|
||||
|
||||
w.WriteHeader(httpErr.StatusCode)
|
||||
_ = bunrouter.JSON(w, httpErr)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
11
middlewares/handlers.go
Normal file
11
middlewares/handlers.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package middlewares
|
||||
|
||||
import "github.com/uptrace/bun"
|
||||
|
||||
type Handlers struct {
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
func NewHandlers(db *bun.DB) *Handlers {
|
||||
return &Handlers{db: db}
|
||||
}
|
||||
13
models/user.go
Normal file
13
models/user.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import "github.com/uptrace/bun"
|
||||
|
||||
type User struct {
|
||||
bun.BaseModel `bun:"table:user" json:"-"`
|
||||
|
||||
Username string `bun:"username,pk" json:"username"`
|
||||
Password string `bun:"password" json:"password"`
|
||||
|
||||
IsLogged bool `bun:"is_logged" json:"isLogged"`
|
||||
LoginCount int `bun:"login_count" json:"loginCount"`
|
||||
}
|
||||
37
player.go
Normal file
37
player.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gitea.konchin.com/ytshih/inp2025/stages"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var playerCmd = &cobra.Command{
|
||||
Use: "player",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
queue := []*tea.Program{}
|
||||
|
||||
base := stages.NewBaseModel(&queue, viper.GetString("auth-endpoint"))
|
||||
queue = append(queue,
|
||||
tea.NewProgram(stages.NewLandingModel(base)))
|
||||
|
||||
for len(queue) > 0 {
|
||||
program := queue[0]
|
||||
queue = queue[1:]
|
||||
_, err := program.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
playerCmd.Flags().
|
||||
Int("udp-listen-port", 18787, "")
|
||||
playerCmd.Flags().
|
||||
StringSlice("udp-endpoints", []string{"localhost:18787"}, "")
|
||||
playerCmd.Flags().
|
||||
String("auth-endpoint", "http://localhost:8888", "")
|
||||
}
|
||||
23
stages/base.go
Normal file
23
stages/base.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package stages
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
queue *[]*tea.Program
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewBaseModel(
|
||||
queue *[]*tea.Program,
|
||||
endpoint string,
|
||||
) *BaseModel {
|
||||
return &BaseModel{
|
||||
queue: queue,
|
||||
client: resty.New().
|
||||
SetBaseURL(endpoint).
|
||||
SetDisableWarn(true),
|
||||
}
|
||||
}
|
||||
241
stages/landing.go
Normal file
241
stages/landing.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package stages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/models"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type landingOperationType int
|
||||
|
||||
const (
|
||||
landingOperationChoose landingOperationType = iota
|
||||
landingOperationLoginCred
|
||||
landingOperationRegistCred
|
||||
)
|
||||
|
||||
type focusTargetType int
|
||||
|
||||
const (
|
||||
focusTargetUsername focusTargetType = iota
|
||||
focusTargetPassword
|
||||
)
|
||||
|
||||
type LandingModel struct {
|
||||
*BaseModel
|
||||
op landingOperationType
|
||||
|
||||
focus focusTargetType
|
||||
username textinput.Model
|
||||
password textinput.Model
|
||||
|
||||
info string
|
||||
err error
|
||||
}
|
||||
|
||||
func NewLandingModel(base *BaseModel) *LandingModel {
|
||||
username := textinput.New()
|
||||
username.Placeholder = "Username"
|
||||
username.CharLimit = 32
|
||||
username.Width = 32
|
||||
username.Blur()
|
||||
|
||||
password := textinput.New()
|
||||
password.Placeholder = "Password"
|
||||
password.CharLimit = 32
|
||||
password.Width = 32
|
||||
password.Blur()
|
||||
password.EchoMode = textinput.EchoPassword
|
||||
password.EchoCharacter = '•'
|
||||
|
||||
return &LandingModel{
|
||||
BaseModel: base,
|
||||
op: landingOperationChoose,
|
||||
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LandingModel) reset() {
|
||||
m.op = landingOperationChoose
|
||||
m.username.Blur()
|
||||
m.username.Reset()
|
||||
m.password.Blur()
|
||||
m.password.Reset()
|
||||
}
|
||||
|
||||
type postLoginMsg struct{}
|
||||
|
||||
func (m *LandingModel) postLogin() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resp, err := m.BaseModel.client.R().
|
||||
SetBasicAuth(m.username.Value(), m.password.Value()).
|
||||
Post("/auth/login")
|
||||
if err == nil {
|
||||
switch resp.StatusCode() {
|
||||
case http.StatusOK:
|
||||
m.BaseModel.client.SetBasicAuth(
|
||||
m.username.Value(), m.password.Value())
|
||||
m.info = "login success.\n"
|
||||
m.err = nil
|
||||
case http.StatusUnauthorized:
|
||||
m.err = fmt.Errorf("user not exist or password incorrect, %s",
|
||||
string(resp.Body()))
|
||||
default:
|
||||
m.err = fmt.Errorf("unknown server error, %s",
|
||||
string(resp.Body()))
|
||||
}
|
||||
} else {
|
||||
m.err = fmt.Errorf("failed to post login, %w", err)
|
||||
}
|
||||
return postLoginMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
type postRegisterMsg struct{}
|
||||
|
||||
func (m *LandingModel) postRegister() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
resp, err := m.BaseModel.client.R().
|
||||
SetBody(models.User{
|
||||
Username: m.username.Value(),
|
||||
Password: m.password.Value(),
|
||||
}).
|
||||
Post("/auth/register")
|
||||
if err == nil {
|
||||
switch resp.StatusCode() {
|
||||
case http.StatusOK:
|
||||
m.info = "register success.\n"
|
||||
m.err = nil
|
||||
case http.StatusBadRequest:
|
||||
m.err = fmt.Errorf("username already exist, %s",
|
||||
string(resp.Body()))
|
||||
default:
|
||||
m.err = fmt.Errorf("unknown server error, %s",
|
||||
string(resp.Body()))
|
||||
}
|
||||
} else {
|
||||
m.err = fmt.Errorf("failed to post register, %s",
|
||||
string(resp.Body()))
|
||||
}
|
||||
return postRegisterMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LandingModel) Init() tea.Cmd {
|
||||
return tea.ClearScreen
|
||||
}
|
||||
|
||||
func (m *LandingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "L", "l":
|
||||
if m.op == landingOperationChoose {
|
||||
m.op = landingOperationLoginCred
|
||||
m.focus = focusTargetUsername
|
||||
cmds = append(cmds, m.username.Focus())
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
case "R", "r":
|
||||
if m.op == landingOperationChoose {
|
||||
m.op = landingOperationRegistCred
|
||||
m.focus = focusTargetUsername
|
||||
cmds = append(cmds, m.username.Focus())
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
case "shift+tab", "up":
|
||||
if m.op == landingOperationLoginCred ||
|
||||
m.op == landingOperationRegistCred {
|
||||
switch m.focus {
|
||||
case focusTargetUsername:
|
||||
m.username.Blur()
|
||||
m.focus = focusTargetPassword
|
||||
cmds = append(cmds, m.password.Focus())
|
||||
case focusTargetPassword:
|
||||
m.password.Blur()
|
||||
m.focus = focusTargetUsername
|
||||
cmds = append(cmds, m.username.Focus())
|
||||
}
|
||||
}
|
||||
case "tab", "down", "enter":
|
||||
if m.op == landingOperationLoginCred ||
|
||||
m.op == landingOperationRegistCred {
|
||||
switch m.focus {
|
||||
case focusTargetUsername:
|
||||
m.username.Blur()
|
||||
m.focus = focusTargetPassword
|
||||
cmds = append(cmds, m.password.Focus())
|
||||
case focusTargetPassword:
|
||||
if msg.String() == "enter" {
|
||||
switch m.op {
|
||||
case landingOperationLoginCred:
|
||||
cmds = append(cmds, m.postLogin())
|
||||
case landingOperationRegistCred:
|
||||
cmds = append(cmds, m.postRegister())
|
||||
}
|
||||
} else {
|
||||
m.password.Blur()
|
||||
m.focus = focusTargetUsername
|
||||
cmds = append(cmds, m.username.Focus())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case postLoginMsg:
|
||||
if m.err == nil {
|
||||
*m.queue = append(*m.queue,
|
||||
tea.NewProgram(NewLobbyModel(m.BaseModel)))
|
||||
return m, tea.Quit
|
||||
} else {
|
||||
m.reset()
|
||||
}
|
||||
case postRegisterMsg:
|
||||
m.reset()
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.username, cmd = m.username.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m.password, cmd = m.password.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *LandingModel) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
switch m.op {
|
||||
case landingOperationChoose:
|
||||
fmt.Fprintf(&b, "Choose Operation\n(L)ogin / (R)egister\n")
|
||||
case landingOperationLoginCred:
|
||||
fmt.Fprintf(&b, "Login: \n")
|
||||
b.WriteString(m.username.View() + "\n")
|
||||
b.WriteString(m.password.View() + "\n")
|
||||
case landingOperationRegistCred:
|
||||
fmt.Fprintf(&b, "Register: \n")
|
||||
b.WriteString(m.username.View() + "\n")
|
||||
b.WriteString(m.password.View() + "\n")
|
||||
}
|
||||
|
||||
if m.info != "" {
|
||||
b.WriteString(m.info + "\n")
|
||||
}
|
||||
|
||||
if m.err != nil {
|
||||
b.WriteString("----------\n")
|
||||
b.WriteString(m.err.Error() + "\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
283
stages/lobby.go
Normal file
283
stages/lobby.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package stages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
"gitea.konchin.com/ytshih/inp2025/workflows"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
REFRESH_TIME = time.Second
|
||||
)
|
||||
|
||||
type lobbyOperationType int
|
||||
|
||||
const (
|
||||
lobbyOperationChoose lobbyOperationType = iota
|
||||
lobbyOperationServerWaiting
|
||||
lobbyOperationServerChoose
|
||||
lobbyOperationClientScannning
|
||||
)
|
||||
|
||||
type LobbyModel struct {
|
||||
*BaseModel
|
||||
op lobbyOperationType
|
||||
|
||||
shutdown types.ShutdownFunc
|
||||
local string
|
||||
remote string
|
||||
|
||||
// server
|
||||
remoteUser string
|
||||
listener net.Listener
|
||||
|
||||
// client
|
||||
cursor int
|
||||
endpoints []string
|
||||
|
||||
info string
|
||||
err error
|
||||
}
|
||||
|
||||
func NewLobbyModel(base *BaseModel) *LobbyModel {
|
||||
return &LobbyModel{
|
||||
BaseModel: base,
|
||||
op: lobbyOperationChoose,
|
||||
|
||||
shutdown: func() {},
|
||||
}
|
||||
}
|
||||
|
||||
type serverListenMsg struct{}
|
||||
|
||||
func (m *LobbyModel) serverListen() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
dataCh := make(chan string)
|
||||
m.local, m.shutdown, m.err = utils.ListenUDPData(
|
||||
viper.GetInt("udp-listen-port"), dataCh)
|
||||
if m.err != nil {
|
||||
m.err = fmt.Errorf("failed to listen, %w", m.err)
|
||||
return serverListenMsg{}
|
||||
}
|
||||
|
||||
req := <-dataCh
|
||||
m.shutdown()
|
||||
m.shutdown = func() {}
|
||||
|
||||
var joinRequest types.JoinRequest
|
||||
if err := msgpack.Unmarshal([]byte(req), &joinRequest); err != nil {
|
||||
m.err = fmt.Errorf("failed to unmarshal msgpack, %w", err)
|
||||
return serverListenMsg{}
|
||||
}
|
||||
|
||||
m.remote = joinRequest.Endpoint
|
||||
m.remoteUser = joinRequest.Username
|
||||
return serverListenMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
type serverSendReplyMsg struct{}
|
||||
|
||||
func (m *LobbyModel) serverSendReply(response bool) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if response {
|
||||
// Start Wordle Server listener
|
||||
m.listener, m.err = net.Listen("tcp4", ":0")
|
||||
if m.err != nil {
|
||||
m.err = fmt.Errorf("failed to listen on anonymous port, %w", m.err)
|
||||
return serverSendReplyMsg{}
|
||||
}
|
||||
|
||||
local := fmt.Sprintf("%s:%d",
|
||||
m.listener.Addr().(*net.TCPAddr).IP.String(),
|
||||
m.listener.Addr().(*net.TCPAddr).Port)
|
||||
m.err = utils.SendPayload(local, m.remote,
|
||||
types.JoinResponse{Endpoint: local})
|
||||
|
||||
// Store wordle server endpoint
|
||||
m.remote = local
|
||||
} else {
|
||||
m.err = utils.SendPayload("", m.remote,
|
||||
types.JoinResponse{Endpoint: ""})
|
||||
}
|
||||
return serverSendReplyMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
type clientScanMsg time.Time
|
||||
|
||||
func (m *LobbyModel) clientScan() tea.Cmd {
|
||||
return tea.Tick(REFRESH_TIME, func(t time.Time) tea.Msg {
|
||||
m.endpoints = []string{}
|
||||
for _, endpoint := range viper.GetStringSlice("udp-endpoints") {
|
||||
if err := utils.Ping(endpoint); err == nil {
|
||||
m.endpoints = append(m.endpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return clientScanMsg(t)
|
||||
})
|
||||
}
|
||||
|
||||
type clientJoinMsg struct{}
|
||||
|
||||
func (m *LobbyModel) clientJoin() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
dataCh := make(chan string)
|
||||
m.local, m.shutdown, m.err = utils.ListenUDPData(0, dataCh)
|
||||
if m.err != nil {
|
||||
m.err = fmt.Errorf("failed to listen udp data, %w", m.err)
|
||||
return clientJoinMsg{}
|
||||
}
|
||||
|
||||
m.err = utils.SendPayload(m.local, m.remote, types.JoinRequest{
|
||||
Endpoint: m.local,
|
||||
Username: m.client.UserInfo.Username,
|
||||
})
|
||||
if m.err != nil {
|
||||
m.err = fmt.Errorf("failed to send invitation, %w", m.err)
|
||||
return clientJoinMsg{}
|
||||
}
|
||||
|
||||
data := <-dataCh
|
||||
m.shutdown()
|
||||
m.shutdown = func() {}
|
||||
|
||||
var joinResponse types.JoinResponse
|
||||
if err := msgpack.Unmarshal([]byte(data), &joinResponse); err != nil {
|
||||
m.err = fmt.Errorf("failed to unmarshal msgpack, %w", err)
|
||||
return clientJoinMsg{}
|
||||
}
|
||||
|
||||
m.remote = joinResponse.Endpoint
|
||||
return clientJoinMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LobbyModel) Init() tea.Cmd {
|
||||
return tea.ClearScreen
|
||||
}
|
||||
|
||||
func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
switch m.op {
|
||||
case lobbyOperationChoose:
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "S", "s":
|
||||
m.op = lobbyOperationServerWaiting
|
||||
cmds = append(cmds, m.serverListen())
|
||||
case "C", "c":
|
||||
m.op = lobbyOperationClientScannning
|
||||
cmds = append(cmds, m.clientScan())
|
||||
}
|
||||
}
|
||||
case lobbyOperationServerWaiting:
|
||||
switch msg.(type) {
|
||||
case serverListenMsg:
|
||||
m.op = lobbyOperationServerChoose
|
||||
}
|
||||
case lobbyOperationServerChoose:
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "Y", "y", "enter":
|
||||
cmds = append(cmds, m.serverSendReply(true))
|
||||
case "N", "n":
|
||||
m.op = lobbyOperationServerWaiting
|
||||
cmds = append(cmds, m.serverSendReply(false))
|
||||
}
|
||||
case serverSendReplyMsg:
|
||||
if m.err == nil {
|
||||
m.shutdown()
|
||||
m.client.SetBaseURL("http://" + m.remote)
|
||||
shutdown := workflows.WordleServer(m.listener)
|
||||
*m.queue = append(*m.queue,
|
||||
tea.NewProgram(NewWordleClientModel(m.BaseModel, shutdown)))
|
||||
return m, tea.Quit
|
||||
} else {
|
||||
m.op = lobbyOperationServerWaiting
|
||||
}
|
||||
}
|
||||
case lobbyOperationClientScannning:
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "shift+tab", "up":
|
||||
if n := len(m.endpoints); n > 0 {
|
||||
m.cursor = (m.cursor - 1 + n) % n
|
||||
}
|
||||
case "tab", "down":
|
||||
if n := len(m.endpoints); n > 0 {
|
||||
m.cursor = (m.cursor + 1) % n
|
||||
}
|
||||
case "enter":
|
||||
m.remote = m.endpoints[m.cursor]
|
||||
cmds = append(cmds, m.clientJoin())
|
||||
}
|
||||
case clientScanMsg:
|
||||
cmds = append(cmds, m.clientScan())
|
||||
case clientJoinMsg:
|
||||
if m.err == nil && m.remote != "" {
|
||||
m.shutdown()
|
||||
m.client.SetBaseURL("http://" + m.remote)
|
||||
*m.BaseModel.queue = append(*m.BaseModel.queue,
|
||||
tea.NewProgram(NewWordleClientModel(m.BaseModel, func() {})))
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *LobbyModel) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
switch m.op {
|
||||
case lobbyOperationChoose:
|
||||
b.WriteString("Choose Role\n(S)erver / (C)lient\n")
|
||||
case lobbyOperationServerWaiting:
|
||||
b.WriteString("Wait for user to join...\n")
|
||||
case lobbyOperationServerChoose:
|
||||
fmt.Fprintf(&b, "Receive Join Request by '%s'\nAccept (Y)/N\n",
|
||||
m.remoteUser)
|
||||
case lobbyOperationClientScannning:
|
||||
b.WriteString("Scanning server...\nChoose one to join\n")
|
||||
for i, endpoint := range m.endpoints {
|
||||
if i == m.cursor {
|
||||
fmt.Fprintf(&b, "(•) %s\n", endpoint)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "( ) %s\n", endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.info != "" {
|
||||
b.WriteString(m.info + "\n")
|
||||
}
|
||||
|
||||
if m.err != nil {
|
||||
b.WriteString("----------\n")
|
||||
b.WriteString(m.err.Error() + "\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
1
stages/udp.go
Normal file
1
stages/udp.go
Normal file
@@ -0,0 +1 @@
|
||||
package stages
|
||||
179
stages/wordle.go
Normal file
179
stages/wordle.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package stages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/handlers/wordle"
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
WEBSOCKET_RETRY int = 5
|
||||
WEBSOCKET_BACKOFF time.Duration = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type WordleClientModel struct {
|
||||
*BaseModel
|
||||
conn *websocket.Conn
|
||||
state types.WordleState
|
||||
shutdown types.ShutdownFunc
|
||||
|
||||
input textinput.Model
|
||||
err error
|
||||
}
|
||||
|
||||
func NewWordleClientModel(
|
||||
base *BaseModel,
|
||||
shutdown types.ShutdownFunc,
|
||||
) *WordleClientModel {
|
||||
input := textinput.New()
|
||||
input.Focus()
|
||||
input.CharLimit = types.GUESS_WORD_LENGTH
|
||||
input.Width = types.GUESS_WORD_LENGTH
|
||||
return &WordleClientModel{
|
||||
BaseModel: base,
|
||||
input: input,
|
||||
shutdown: shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WordleClientModel) getState() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.conn == nil {
|
||||
u, err := url.Parse(m.client.BaseURL)
|
||||
if err != nil {
|
||||
m.err = fmt.Errorf("failed to parse BaseURL, %w", err)
|
||||
}
|
||||
u.Scheme = "ws"
|
||||
u.Path = "/api/state"
|
||||
|
||||
for try := 0; try < WEBSOCKET_RETRY; try++ {
|
||||
req, _ := http.NewRequest("GET",
|
||||
/*placeholder*/ "http://localhost", nil)
|
||||
req.SetBasicAuth(
|
||||
m.client.UserInfo.Username,
|
||||
m.client.UserInfo.Password)
|
||||
m.conn, _, err = websocket.DefaultDialer.Dial(
|
||||
u.String(), req.Header)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(WEBSOCKET_BACKOFF)
|
||||
}
|
||||
if err != nil {
|
||||
m.err = fmt.Errorf("failed to dial, %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, b, err := m.conn.ReadMessage()
|
||||
if err == nil {
|
||||
var state types.WordleState
|
||||
if err := msgpack.Unmarshal(b, &state); err != nil {
|
||||
m.err = fmt.Errorf("failed to unmarshal state, %w", err)
|
||||
}
|
||||
return state
|
||||
} else {
|
||||
m.err = fmt.Errorf("failed to read message, %w", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WordleClientModel) postGuess(guess string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
b, err := msgpack.Marshal(wordle.OperationGuess{
|
||||
Username: m.client.UserInfo.Username,
|
||||
Guess: guess,
|
||||
})
|
||||
if err != nil {
|
||||
m.err = fmt.Errorf("failed to post guess, %w", err)
|
||||
}
|
||||
_, err = m.client.R().
|
||||
SetBody(b).
|
||||
Post("/api/guess")
|
||||
if err != nil {
|
||||
m.err = fmt.Errorf("failed to post guess, %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WordleClientModel) Init() tea.Cmd {
|
||||
return tea.Sequence(tea.ClearScreen,
|
||||
tea.Batch(m.getState(), textinput.Blink))
|
||||
}
|
||||
|
||||
func (m *WordleClientModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
m.conn.Close()
|
||||
m.shutdown()
|
||||
return m, tea.Quit
|
||||
case "enter":
|
||||
if m.state.GameEnd {
|
||||
m.conn.Close()
|
||||
m.shutdown()
|
||||
*m.queue = append(*m.queue,
|
||||
tea.NewProgram(NewLobbyModel(m.BaseModel)))
|
||||
return m, tea.Quit
|
||||
}
|
||||
if len(m.input.Value()) == types.GUESS_WORD_LENGTH {
|
||||
input := strings.ToUpper(m.input.Value())
|
||||
m.input.Reset()
|
||||
cmds = append(cmds, m.postGuess(input))
|
||||
}
|
||||
}
|
||||
case types.WordleState:
|
||||
m.state = msg
|
||||
if m.state.GameEnd {
|
||||
m.input.Blur()
|
||||
}
|
||||
if _, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok {
|
||||
m.input.Blur()
|
||||
} else {
|
||||
cmds = append(cmds, m.input.Focus())
|
||||
}
|
||||
cmds = append(cmds, m.getState())
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.input, cmd = m.input.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *WordleClientModel) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "Login as '%s'\n", m.client.UserInfo.Username)
|
||||
fmt.Fprintf(&b, "remote addr: %s\n", m.client.BaseURL)
|
||||
|
||||
b.WriteString(m.state.View() + "\n")
|
||||
b.WriteString(types.NewWordBank(m.state).View() + "\n")
|
||||
|
||||
if m.state.GameEnd {
|
||||
fmt.Fprintf(&b, "Game End. Press 'Enter' to lobby.\n")
|
||||
} else {
|
||||
if guess, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok {
|
||||
fmt.Fprintf(&b, "Current guess for the round: %s\n", guess)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "guess: %s\n", m.input.View())
|
||||
}
|
||||
if m.err != nil {
|
||||
fmt.Fprintf(&b, "error: %+v\n", m.err)
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
38
test.go
Normal file
38
test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var testCmd = &cobra.Command{
|
||||
Use: "test",
|
||||
}
|
||||
|
||||
var testServerCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
dataCh := make(chan string)
|
||||
_, shutdown, err := utils.ListenUDPData(18787, dataCh)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer shutdown()
|
||||
<-dataCh
|
||||
},
|
||||
}
|
||||
|
||||
var testClientCmd = &cobra.Command{
|
||||
Use: "client",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := utils.Ping("localhost:18787")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
testCmd.AddCommand(testServerCmd)
|
||||
testCmd.AddCommand(testClientCmd)
|
||||
}
|
||||
7
types/auth.go
Normal file
7
types/auth.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package types
|
||||
|
||||
type UserType struct{}
|
||||
|
||||
var (
|
||||
UserKey = UserType{}
|
||||
)
|
||||
5
types/types.go
Normal file
5
types/types.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package types
|
||||
|
||||
type ShutdownFunc func()
|
||||
|
||||
type UsernameType = string
|
||||
10
types/udp.go
Normal file
10
types/udp.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package types
|
||||
|
||||
type JoinRequest struct {
|
||||
Endpoint string
|
||||
Username UsernameType
|
||||
}
|
||||
|
||||
type JoinResponse struct {
|
||||
Endpoint string
|
||||
}
|
||||
209
types/wordle.go
Normal file
209
types/wordle.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
const (
|
||||
GUESS_COUNT_LIMIT int = 6
|
||||
GUESS_WORD_LENGTH int = 5
|
||||
|
||||
ALPHABET_COUNT int = 26
|
||||
)
|
||||
|
||||
type GuessStateType int
|
||||
|
||||
const (
|
||||
GuessStateNotGuessed GuessStateType = iota
|
||||
GuessStateWrongChar
|
||||
GuessStateWrongPos
|
||||
GuessStateCorrect
|
||||
)
|
||||
|
||||
type GuessChar struct {
|
||||
Char rune
|
||||
State GuessStateType
|
||||
}
|
||||
|
||||
func (self GuessChar) View() string {
|
||||
style := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
switch self.State {
|
||||
case GuessStateWrongChar:
|
||||
style = style.Background(lipgloss.Color("#3a3a3c"))
|
||||
case GuessStateWrongPos:
|
||||
style = style.Background(lipgloss.Color("#b59f3b"))
|
||||
case GuessStateCorrect:
|
||||
style = style.Background(lipgloss.Color("#538d4e"))
|
||||
}
|
||||
return style.Render(string(self.Char))
|
||||
}
|
||||
|
||||
type Guess struct {
|
||||
Data [GUESS_WORD_LENGTH]GuessChar
|
||||
}
|
||||
|
||||
type guessLeftType struct {
|
||||
Rune rune
|
||||
Idx int
|
||||
}
|
||||
|
||||
func NewGuess(guess, ans string) Guess {
|
||||
var ret Guess
|
||||
|
||||
var ansLeft []rune
|
||||
var guessLeft []guessLeftType
|
||||
for i := range ret.Data {
|
||||
ret.Data[i] = GuessChar{
|
||||
Char: rune(guess[i]),
|
||||
State: GuessStateWrongChar,
|
||||
}
|
||||
if guess[i] == ans[i] {
|
||||
ret.Data[i].State = GuessStateCorrect
|
||||
} else {
|
||||
ansLeft = append(ansLeft, rune(ans[i]))
|
||||
guessLeft = append(guessLeft, guessLeftType{
|
||||
Rune: rune(guess[i]),
|
||||
Idx: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(ansLeft, func(i, j int) bool {
|
||||
return int32(ansLeft[i]) < int32(ansLeft[j])
|
||||
})
|
||||
|
||||
sort.Slice(guessLeft, func(i, j int) bool {
|
||||
return int32(guessLeft[i].Rune) < int32(guessLeft[j].Rune)
|
||||
})
|
||||
|
||||
i, j := 0, 0
|
||||
for i < len(ansLeft) && j < len(guessLeft) {
|
||||
if int32(ansLeft[i]) == int32(guessLeft[j].Rune) {
|
||||
ret.Data[guessLeft[j].Idx].State = GuessStateWrongPos
|
||||
i++
|
||||
j++
|
||||
} else {
|
||||
if int32(ansLeft[i]) < int32(guessLeft[j].Rune) {
|
||||
i++
|
||||
} else {
|
||||
j++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (self Guess) View() string {
|
||||
var b strings.Builder
|
||||
for _, ch := range self.Data {
|
||||
b.WriteString(ch.View())
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type WordleState struct {
|
||||
History map[UsernameType][GUESS_COUNT_LIMIT]Guess
|
||||
CurrentGuess map[UsernameType]string
|
||||
|
||||
Round int
|
||||
GameEnd bool
|
||||
}
|
||||
|
||||
func NewWordleState() WordleState {
|
||||
return WordleState{
|
||||
History: make(map[UsernameType][GUESS_COUNT_LIMIT]Guess),
|
||||
CurrentGuess: make(map[UsernameType]string),
|
||||
|
||||
Round: 0,
|
||||
GameEnd: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WordleState) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
col := []string{}
|
||||
for user := range self.History {
|
||||
col = append(col, user)
|
||||
}
|
||||
|
||||
sort.Slice(col, func(i, j int) bool {
|
||||
return col[i] < col[j]
|
||||
})
|
||||
|
||||
fmt.Fprintf(&b, " ")
|
||||
for _, user := range col {
|
||||
fmt.Fprintf(&b, "%5s ", user)
|
||||
}
|
||||
b.WriteRune('\n')
|
||||
|
||||
table := make([][]string, GUESS_COUNT_LIMIT)
|
||||
for i := range table {
|
||||
table[i] = make([]string, len(col))
|
||||
}
|
||||
for j, user := range col {
|
||||
for i, guess := range self.History[user] {
|
||||
table[i][j] = guess.View()
|
||||
}
|
||||
}
|
||||
|
||||
for i, row := range table {
|
||||
fmt.Fprintf(&b, "%1d ", i+1)
|
||||
for _, e := range row {
|
||||
b.WriteString(e + " ")
|
||||
}
|
||||
b.WriteRune('\n')
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type WordBank struct {
|
||||
Data [ALPHABET_COUNT]GuessStateType
|
||||
}
|
||||
|
||||
func NewWordBank(state WordleState) WordBank {
|
||||
// FIXME: breaks when enter ^A-Z
|
||||
var ret WordBank
|
||||
for _, guesses := range state.History {
|
||||
for i := 0; i < state.Round; i++ {
|
||||
for _, ch := range guesses[i].Data {
|
||||
ret.Data[int32(ch.Char)-'A'] = max(
|
||||
ret.Data[int32(ch.Char)-'A'],
|
||||
ch.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (self WordBank) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
for i, state := range self.Data {
|
||||
style := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
switch state {
|
||||
case GuessStateNotGuessed:
|
||||
style = style.Background(lipgloss.Color("#2c3032"))
|
||||
case GuessStateWrongChar:
|
||||
style = style.Background(lipgloss.Color("#3a3a3c"))
|
||||
case GuessStateWrongPos:
|
||||
style = style.Background(lipgloss.Color("#b59f3b"))
|
||||
case GuessStateCorrect:
|
||||
style = style.Background(lipgloss.Color("#538d4e"))
|
||||
}
|
||||
b.WriteString(style.Render(string('A' + i)))
|
||||
if i%10 == 9 {
|
||||
b.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
13
utils/initDB.go
Normal file
13
utils/initDB.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/models"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func InitDB(ctx context.Context, db *bun.DB) error {
|
||||
return db.ResetModel(ctx,
|
||||
(*models.User)(nil))
|
||||
}
|
||||
14
utils/success.go
Normal file
14
utils/success.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Success(w http.ResponseWriter) error {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, err := io.WriteString(w,
|
||||
`{"code":200, "message": "success"}`+"\n")
|
||||
return err
|
||||
}
|
||||
143
utils/udp.go
Normal file
143
utils/udp.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
BUFFER_SIZE int = 1024
|
||||
MAGIC_NUMBER int = 114514
|
||||
)
|
||||
|
||||
type UDPReqType int
|
||||
|
||||
const (
|
||||
UDPReqTypeData UDPReqType = iota
|
||||
UDPReqTypePingRequest
|
||||
UDPReqTypePingReply
|
||||
)
|
||||
|
||||
type UDPPayload struct {
|
||||
MagicNumber int `msgpack:"magicNumber"`
|
||||
Endpoint string `msgpack:"endpoint"`
|
||||
Type UDPReqType `msgpack:"type"`
|
||||
|
||||
Data string `msgpack:"data"`
|
||||
}
|
||||
|
||||
func ListenUDPData(
|
||||
port int,
|
||||
dataCh chan string,
|
||||
) (string, types.ShutdownFunc, error) {
|
||||
return ListenUDP(port, dataCh, nil)
|
||||
}
|
||||
|
||||
func Ping(endpoint string) error {
|
||||
pingCh := make(chan string)
|
||||
local, shutdown, err := ListenUDP(0, nil, pingCh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer shutdown()
|
||||
|
||||
err = SendRawPayload(endpoint, UDPPayload{
|
||||
MagicNumber: MAGIC_NUMBER,
|
||||
Endpoint: local,
|
||||
Type: UDPReqTypePingRequest,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-pingCh
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListenUDP(
|
||||
port int,
|
||||
dataCh chan string,
|
||||
pingCh chan string,
|
||||
) (string, types.ShutdownFunc, error) {
|
||||
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to listen udp, %w", err)
|
||||
}
|
||||
local := conn.LocalAddr().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 {
|
||||
switch payload.Type {
|
||||
case UDPReqTypeData:
|
||||
if dataCh != nil {
|
||||
dataCh <- payload.Data
|
||||
}
|
||||
case UDPReqTypePingRequest:
|
||||
SendRawPayload(payload.Endpoint, UDPPayload{
|
||||
MagicNumber: MAGIC_NUMBER,
|
||||
Endpoint: local,
|
||||
Type: UDPReqTypePingReply,
|
||||
})
|
||||
case UDPReqTypePingReply:
|
||||
if pingCh != nil {
|
||||
pingCh <- payload.Endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return local, func() { conn.Close() }, nil
|
||||
}
|
||||
|
||||
func SendPayload(
|
||||
local, remote string,
|
||||
data any,
|
||||
) error {
|
||||
sdata, err := msgpack.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal data, %w", err)
|
||||
}
|
||||
|
||||
return SendRawPayload(remote, UDPPayload{
|
||||
MagicNumber: MAGIC_NUMBER,
|
||||
Endpoint: local,
|
||||
Type: UDPReqTypeData,
|
||||
Data: string(sdata),
|
||||
})
|
||||
}
|
||||
|
||||
func SendRawPayload(
|
||||
endpoint string,
|
||||
payload UDPPayload,
|
||||
) error {
|
||||
conn, err := net.Dial("udp", endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to dial endpoint, %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
b, err := msgpack.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload, %w", err)
|
||||
}
|
||||
|
||||
_, err = conn.Write(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send payload, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
63
workflows/authServer.go
Normal file
63
workflows/authServer.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/handlers/auth"
|
||||
"gitea.konchin.com/ytshih/inp2025/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/utils"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"github.com/uptrace/bun/driver/sqliteshim"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func AuthServer() {
|
||||
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to init sqlite, %w", err))
|
||||
}
|
||||
db := bun.NewDB(sqldb, sqlitedialect.New())
|
||||
if err := utils.InitDB(context.Background(), db); err != nil {
|
||||
panic(fmt.Errorf("failed to init db schema, %w", err))
|
||||
}
|
||||
|
||||
logger := zap.NewExample()
|
||||
defer logger.Sync()
|
||||
undo := zap.ReplaceGlobals(logger)
|
||||
defer undo()
|
||||
|
||||
authHandlers := auth.NewHandlers(db)
|
||||
middlewareHandlers := middlewares.NewHandlers(db)
|
||||
|
||||
router := bunrouter.New()
|
||||
|
||||
authGroup := router.NewGroup("/auth").
|
||||
Use(middlewareHandlers.ErrorHandler)
|
||||
authGroup.POST("/register",
|
||||
authHandlers.PostRegister)
|
||||
authGroup.POST("/login",
|
||||
middlewareHandlers.Auth(authHandlers.PostLogin))
|
||||
authGroup.POST("/logout",
|
||||
middlewareHandlers.Auth(authHandlers.PostLogout))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: viper.GetString("listen-addr"),
|
||||
Handler: http.Handler(router),
|
||||
}
|
||||
|
||||
defer func() {
|
||||
sqldb.Close()
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
panic(fmt.Errorf("failed to shutdown wordle server, %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Printf("server up\n")
|
||||
fmt.Println(server.ListenAndServe())
|
||||
}
|
||||
86
workflows/wordleServer.go
Normal file
86
workflows/wordleServer.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitea.konchin.com/ytshih/inp2025/handlers/wordle"
|
||||
"gitea.konchin.com/ytshih/inp2025/middlewares"
|
||||
"gitea.konchin.com/ytshih/inp2025/types"
|
||||
"github.com/uptrace/bunrouter"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
DICT_FILE string = "./dict"
|
||||
)
|
||||
|
||||
func generateWordleAns() (string, error) {
|
||||
file, err := os.Open(DICT_FILE)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open dictionary, %w", err)
|
||||
}
|
||||
|
||||
dict := []string{}
|
||||
for {
|
||||
var w string
|
||||
_, err := fmt.Fscanf(file, "%s", &w)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
dict = append(dict, w)
|
||||
}
|
||||
|
||||
return dict[rand.Intn(len(dict))], nil
|
||||
}
|
||||
|
||||
func WordleServer(listener net.Listener) types.ShutdownFunc {
|
||||
ans, err := generateWordleAns()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to generate answer, %w", err))
|
||||
}
|
||||
|
||||
logger, _ := zap.Config{
|
||||
Level: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
Encoding: "json",
|
||||
OutputPaths: []string{"logs/wordle-stdout.log"},
|
||||
ErrorOutputPaths: []string{"logs/wordle-stderr.log"},
|
||||
EncoderConfig: zap.NewProductionEncoderConfig(),
|
||||
}.Build()
|
||||
undo := zap.ReplaceGlobals(logger)
|
||||
|
||||
wordleHandlers := wordle.NewHandlers(ans)
|
||||
middlewareHandlers := middlewares.NewHandlers(nil)
|
||||
|
||||
router := bunrouter.New()
|
||||
|
||||
apiGroup := router.NewGroup("/api").
|
||||
Use(middlewareHandlers.ErrorHandler)
|
||||
apiGroup.GET("/state",
|
||||
wordleHandlers.GetState)
|
||||
apiGroup.POST("/guess",
|
||||
wordleHandlers.PostGuess)
|
||||
|
||||
server := &http.Server{
|
||||
Handler: http.Handler(router),
|
||||
}
|
||||
|
||||
go func() {
|
||||
fmt.Printf("server up\n")
|
||||
if err := server.Serve(listener); err != http.ErrServerClosed {
|
||||
panic(fmt.Errorf("wordle server failed, %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
logger.Sync()
|
||||
undo()
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
panic(fmt.Errorf("failed to shutdown wordle server, %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user