diff --git a/go.mod b/go.mod index b32d219..aaff2de 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/jackc/pgx/v5 v5.6.0 github.com/jlouis/glicko2 v1.0.0 github.com/matcornic/hermes/v2 v2.1.0 - golang.org/x/text v0.16.0 + golang.org/x/text v0.17.0 nhooyr.io/websocket v1.8.11 ) @@ -37,7 +37,7 @@ require ( github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/vanng822/css v1.0.1 // indirect github.com/vanng822/go-premailer v1.21.0 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 2ae2ca6..1204bd3 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -115,12 +115,12 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -133,8 +133,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -150,8 +150,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/pkg/server/database.go b/pkg/server/database.go index 185d3b6..d243e4f 100644 --- a/pkg/server/database.go +++ b/pkg/server/database.go @@ -1019,7 +1019,7 @@ func cumulativeStats(tz *time.Location) (*serverStatsResult, error) { return result, nil } -func botStats(name string, tz *time.Location) (*botStatsResult, error) { +func accountStats(name string, matchType int, variant int8, tz *time.Location) (*accountStatsResult, error) { dbLock.Lock() defer dbLock.Unlock() @@ -1030,7 +1030,7 @@ func botStats(name string, tz *time.Location) (*botStatsResult, error) { defer tx.Commit(context.Background()) var earliestGame int64 - rows, err := tx.Query(context.Background(), "SELECT started FROM game WHERE player1 = $1 OR player2 = $2 ORDER BY started ASC LIMIT 1", name, name) + rows, err := tx.Query(context.Background(), "SELECT started FROM game WHERE (player1 = $1 OR player2 = $2) AND variant = $3 ORDER BY started ASC LIMIT 1", name, name, variant) if err != nil { return nil, err } @@ -1044,12 +1044,13 @@ func botStats(name string, tz *time.Location) (*botStatsResult, error) { return nil, err } - result := &botStatsResult{} - earliest := midnight(time.Unix(earliestGame, 0).In(tz)) - rangeStart, rangeEnd := earliest.Unix(), earliest.AddDate(0, 0, 1).Unix() + result := &accountStatsResult{} + m := midnight(time.Unix(earliestGame, 0).In(tz)) + earliest := time.Date(m.Year(), m.Month(), 1, 0, 0, 0, 0, m.Location()) + rangeStart, rangeEnd := earliest.Unix(), earliest.AddDate(0, 1, -(earliest.Day()-1)).Unix() var winCount, lossCount int for { - rows, err := tx.Query(context.Background(), "SELECT COUNT(*) FROM game WHERE started >= $1 AND started < $2 AND (player1 = $3 OR player2 = $4)", rangeStart, rangeEnd, name, name) + rows, err := tx.Query(context.Background(), "SELECT COUNT(*) FROM game WHERE started >= $1 AND started < $2 AND (player1 = $3 OR player2 = $4) AND variant = $5", rangeStart, rangeEnd, name, name, variant) if err != nil { return nil, err } @@ -1063,7 +1064,7 @@ func botStats(name string, tz *time.Location) (*botStatsResult, error) { return nil, err } - rows, err = tx.Query(context.Background(), "SELECT COUNT(*) FROM game WHERE started >= $1 AND started < $2 AND ((player1 = $3 AND winner = 1) OR (player2 = $4 AND winner = 2))", rangeStart, rangeEnd, name, name) + rows, err = tx.Query(context.Background(), "SELECT COUNT(*) FROM game WHERE started >= $1 AND started < $2 AND ((player1 = $3 AND winner = 1) OR (player2 = $4 AND winner = 2)) AND variant = $5", rangeStart, rangeEnd, name, name, variant) if err != nil { return nil, err } @@ -1079,16 +1080,16 @@ func botStats(name string, tz *time.Location) (*botStatsResult, error) { lossCount -= winCount if winCount != 0 || lossCount != 0 { - result.History = append(result.History, &botStatsEntry{ - Date: earliest.Format("2006-01-02"), + result.History = append(result.History, &accountStatsEntry{ + Date: earliest.Format("2006-01"), Percent: float64(winCount) / float64(winCount+lossCount), Wins: winCount, Losses: lossCount, }) } - earliest = earliest.AddDate(0, 0, 1) - rangeStart, rangeEnd = rangeEnd, earliest.AddDate(0, 0, 1).Unix() + earliest = time.Date(earliest.Year(), earliest.Month()+1, 1, 0, 0, 0, 0, m.Location()) + rangeStart, rangeEnd = rangeEnd, earliest.Unix() if rangeStart >= time.Now().Unix() { break } diff --git a/pkg/server/database_common.go b/pkg/server/database_common.go index f4c6cb8..275c470 100644 --- a/pkg/server/database_common.go +++ b/pkg/server/database_common.go @@ -27,13 +27,13 @@ type serverStatsResult struct { History []*serverStatsEntry } -type botStatsEntry struct { +type accountStatsEntry struct { Date string Percent float64 Wins int Losses int } -type botStatsResult struct { - History []*botStatsEntry +type accountStatsResult struct { + History []*accountStatsEntry } diff --git a/pkg/server/database_disabled.go b/pkg/server/database_disabled.go index 7a3bd80..c6bfe2b 100644 --- a/pkg/server/database_disabled.go +++ b/pkg/server/database_disabled.go @@ -87,6 +87,6 @@ func cumulativeStats(tz *time.Location) (*serverStatsResult, error) { return &serverStatsResult{}, nil } -func botStats(name string, tz *time.Location) (*botStatsResult, error) { - return &botStatsResult{}, nil +func accountStats(name string, tz *time.Location) (*accountStatsResult, error) { + return &accountStatsResult{}, nil } diff --git a/pkg/server/server_full.go b/pkg/server/server_full.go index 54f80d8..d6785e3 100644 --- a/pkg/server/server_full.go +++ b/pkg/server/server_full.go @@ -47,24 +47,28 @@ func (s *server) listenWebSocket(address string) { m := mux.NewRouter() m.HandleFunc("/reset/{id:[0-9]+}/{key:[A-Za-z0-9]+}", s.handleResetPassword) m.HandleFunc("/match/{id:[0-9]+}", s.handleMatch) - m.HandleFunc("/matches", s.handleListMatches) - m.HandleFunc("/leaderboard-casual-backgammon-single", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantBackgammon, false)) - m.HandleFunc("/leaderboard-casual-backgammon-multi", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantBackgammon, true)) - m.HandleFunc("/leaderboard-casual-acey-single", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantAceyDeucey, false)) - m.HandleFunc("/leaderboard-casual-acey-multi", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantAceyDeucey, true)) - m.HandleFunc("/leaderboard-casual-tabula-single", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantTabula, false)) - m.HandleFunc("/leaderboard-casual-tabula-multi", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantTabula, true)) - m.HandleFunc("/leaderboard-rated-backgammon-single", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantBackgammon, false)) - m.HandleFunc("/leaderboard-rated-backgammon-multi", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantBackgammon, true)) - m.HandleFunc("/leaderboard-rated-acey-single", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantAceyDeucey, false)) - m.HandleFunc("/leaderboard-rated-acey-multi", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantAceyDeucey, true)) - m.HandleFunc("/leaderboard-rated-tabula-single", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantTabula, false)) - m.HandleFunc("/leaderboard-rated-tabula-multi", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantTabula, true)) - m.HandleFunc("/stats", s.handleStatsFunc(0)) - m.HandleFunc("/stats-month", s.handleStatsFunc(1)) - m.HandleFunc("/stats-total", s.handleStatsFunc(2)) - m.HandleFunc("/stats-tabula", s.handleStatsFunc(3)) - m.HandleFunc("/stats-wildbg", s.handleStatsFunc(4)) + m.HandleFunc("/matches.json", s.handleListMatches) + m.HandleFunc("/leaderboard-casual-backgammon-single.json", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantBackgammon, false)) + m.HandleFunc("/leaderboard-casual-backgammon-multi.json", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantBackgammon, true)) + m.HandleFunc("/leaderboard-casual-acey-single.json", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantAceyDeucey, false)) + m.HandleFunc("/leaderboard-casual-acey-multi.json", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantAceyDeucey, true)) + m.HandleFunc("/leaderboard-casual-tabula-single.json", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantTabula, false)) + m.HandleFunc("/leaderboard-casual-tabula-multi.json", s.handleLeaderboardFunc(matchTypeCasual, bgammon.VariantTabula, true)) + m.HandleFunc("/leaderboard-rated-backgammon-single.json", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantBackgammon, false)) + m.HandleFunc("/leaderboard-rated-backgammon-multi.json", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantBackgammon, true)) + m.HandleFunc("/leaderboard-rated-acey-single.json", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantAceyDeucey, false)) + m.HandleFunc("/leaderboard-rated-acey-multi.json", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantAceyDeucey, true)) + m.HandleFunc("/leaderboard-rated-tabula-single.json", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantTabula, false)) + m.HandleFunc("/leaderboard-rated-tabula-multi.json", s.handleLeaderboardFunc(matchTypeRated, bgammon.VariantTabula, true)) + m.HandleFunc("/stats.json", s.handleStatsFunc(0)) + m.HandleFunc("/stats-month.json", s.handleStatsFunc(1)) + m.HandleFunc("/stats-total.json", s.handleStatsFunc(2)) + m.HandleFunc("/stats-tabula.json", s.handleStatsFunc(3)) + m.HandleFunc("/stats-wildbg.json", s.handleStatsFunc(4)) + m.HandleFunc("/stats/{username:[A-Za-z0-9_\\-]+}.json", s.handleAccountStatsFunc(matchTypeCasual, bgammon.VariantBackgammon)) + m.HandleFunc("/stats/{username:[A-Za-z0-9_\\-]+}/backgammon.json", s.handleAccountStatsFunc(matchTypeCasual, bgammon.VariantBackgammon)) + m.HandleFunc("/stats/{username:[A-Za-z0-9_\\-]+}/acey.json", s.handleAccountStatsFunc(matchTypeCasual, bgammon.VariantAceyDeucey)) + m.HandleFunc("/stats/{username:[A-Za-z0-9_\\-]+}/tabula.json", s.handleAccountStatsFunc(matchTypeCasual, bgammon.VariantTabula)) m.HandleFunc("/", s.handleWebSocket) err := http.ListenAndServe(address, m) @@ -204,25 +208,25 @@ func (s *server) cachedStats(statsType int) []byte { } s.statsCache[statsType], err = json.Marshal(stats) if err != nil { - log.Fatalf("failed to fetch serialize server statistics: %s", err) + log.Fatalf("failed to serialize server statistics: %s", err) } case 3: - stats, err := botStats("BOT_tabula", s.tz) + stats, err := accountStats("BOT_tabula", matchTypeCasual, bgammon.VariantBackgammon, s.tz) if err != nil { log.Fatalf("failed to fetch tabula statistics: %s", err) } s.statsCache[statsType], err = json.Marshal(stats) if err != nil { - log.Fatalf("failed to fetch serialize tabula statistics: %s", err) + log.Fatalf("failed to serialize tabula statistics: %s", err) } default: - stats, err := botStats("BOT_wildbg", s.tz) + stats, err := accountStats("BOT_wildbg", matchTypeCasual, bgammon.VariantBackgammon, s.tz) if err != nil { log.Fatalf("failed to fetch wildbg statistics: %s", err) } s.statsCache[statsType], err = json.Marshal(stats) if err != nil { - log.Fatalf("failed to fetch serialize wildbg statistics: %s", err) + log.Fatalf("failed to serialize wildbg statistics: %s", err) } } @@ -273,6 +277,33 @@ func (s *server) handleListMatches(w http.ResponseWriter, r *http.Request) { w.Write(s.cachedMatches()) } +func (s *server) handleAccountStatsFunc(matchType int, variant int8) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + username := strings.ToLower(strings.TrimSpace(vars["username"])) + if username == "" { + w.Write([]byte(`

No account specified.

`)) + return + } + if strings.HasPrefix(username, "guest_") { + username = "Guest_" + username[6:] + } else if strings.HasPrefix(username, "bot_") { + username = "BOT_" + username[4:] + } + w.Header().Set("Content-Type", "application/json") + stats, err := accountStats(username, matchType, variant, s.tz) + if err != nil { + log.Fatalf("failed to fetch account statistics: %s", err) + } + buf, err := json.Marshal(stats) + if err != nil { + log.Fatalf("failed to serialize account statistics: %s", err) + } + w.Write(buf) + + } +} + func (s *server) handleLeaderboardFunc(matchType int, variant int8, multiPoint bool) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json")