Author SHA1 Message Date
Flashfyre 2547ca173a Fix voucher compensation logic 2024-04-10 16:45:01 -04:00
34 changed files with 1036 additions and 1167 deletions

No files matched your search

-28
View File
@@ -1,28 +0,0 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
+1 -3
View File
@@ -1,6 +1,4 @@
# no extension on linux, .exe on windows pokerogue-server.exe
pokerogue-server*
userdata/* userdata/*
secret.key secret.key
www/
+53
View File
@@ -0,0 +1,53 @@
package api
import (
"encoding/base64"
"fmt"
"net/http"
"github.com/Flashfyre/pokerogue-server/db"
)
func getUsernameFromRequest(r *http.Request) (string, error) {
if r.Header.Get("Authorization") == "" {
return "", fmt.Errorf("missing token")
}
token, err := base64.StdEncoding.DecodeString(r.Header.Get("Authorization"))
if err != nil {
return "", fmt.Errorf("failed to decode token: %s", err)
}
if len(token) != 32 {
return "", fmt.Errorf("invalid token length: got %d, expected 32", len(token))
}
username, err := db.FetchUsernameFromToken(token)
if err != nil {
return "", fmt.Errorf("failed to validate token: %s", err)
}
return username, nil
}
func getUUIDFromRequest(r *http.Request) ([]byte, error) {
if r.Header.Get("Authorization") == "" {
return nil, fmt.Errorf("missing token")
}
token, err := base64.StdEncoding.DecodeString(r.Header.Get("Authorization"))
if err != nil {
return nil, fmt.Errorf("failed to decode token: %s", err)
}
if len(token) != 32 {
return nil, fmt.Errorf("invalid token length: got %d, expected 32", len(token))
}
uuid, err := db.FetchUUIDFromToken(token)
if err != nil {
return nil, fmt.Errorf("failed to validate token: %s", err)
}
return uuid, nil
}
+146
View File
@@ -0,0 +1,146 @@
package api
import (
"bytes"
"crypto/rand"
"database/sql"
"encoding/base64"
"fmt"
"os"
"regexp"
"strconv"
"time"
"github.com/Flashfyre/pokerogue-server/db"
"golang.org/x/crypto/argon2"
)
const (
UUIDSize = 16
ArgonTime = 1
ArgonMemory = 256 * 1024
ArgonThreads = 4
ArgonKeySize = 32
ArgonSaltSize = 16
)
var isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString
type AccountInfoResponse struct {
Username string `json:"username"`
LastSessionSlot int `json:"lastSessionSlot"`
}
// /account/info - get account info
func handleAccountInfo(username string, uuid []byte) (AccountInfoResponse, error) {
var latestSave time.Time
latestSaveID := -1
for id := range sessionSlotCount {
fileName := "session"
if id != 0 {
fileName += strconv.Itoa(id)
}
stat, err := os.Stat(fmt.Sprintf("userdata/%x/%s.pzs", uuid, fileName))
if err != nil {
continue
}
if stat.ModTime().After(latestSave) {
latestSave = stat.ModTime()
latestSaveID = id
}
}
return AccountInfoResponse{Username: username, LastSessionSlot: latestSaveID}, nil
}
type AccountRegisterRequest GenericAuthRequest
// /account/register - register account
func handleAccountRegister(username, password string) error {
if !isValidUsername(username) {
return fmt.Errorf("invalid username")
}
if len(password) < 6 {
return fmt.Errorf("invalid password")
}
uuid := make([]byte, UUIDSize)
_, err := rand.Read(uuid)
if err != nil {
return fmt.Errorf("failed to generate uuid: %s", err)
}
salt := make([]byte, ArgonSaltSize)
_, err = rand.Read(salt)
if err != nil {
return fmt.Errorf(fmt.Sprintf("failed to generate salt: %s", err))
}
err = db.AddAccountRecord(uuid, username, argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize), salt)
if err != nil {
return fmt.Errorf("failed to add account record: %s", err)
}
return nil
}
type AccountLoginRequest GenericAuthRequest
type AccountLoginResponse GenericAuthResponse
// /account/login - log into account
func handleAccountLogin(username, password string) (AccountLoginResponse, error) {
if !isValidUsername(username) {
return AccountLoginResponse{}, fmt.Errorf("invalid username")
}
if len(password) < 6 {
return AccountLoginResponse{}, fmt.Errorf("invalid password")
}
key, salt, err := db.FetchAccountKeySaltFromUsername(username)
if err != nil {
if err == sql.ErrNoRows {
return AccountLoginResponse{}, fmt.Errorf("account doesn't exist")
}
return AccountLoginResponse{}, err
}
if !bytes.Equal(key, argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize)) {
return AccountLoginResponse{}, fmt.Errorf("password doesn't match")
}
token := make([]byte, 32)
_, err = rand.Read(token)
if err != nil {
return AccountLoginResponse{}, fmt.Errorf("failed to generate token: %s", err)
}
err = db.AddAccountSession(username, token)
if err != nil {
return AccountLoginResponse{}, fmt.Errorf("failed to add account session")
}
return AccountLoginResponse{Token: base64.StdEncoding.EncodeToString(token)}, nil
}
// /account/logout - log out of account
func handleAccountLogout(token []byte) error {
if len(token) != 32 {
return fmt.Errorf("invalid token")
}
err := db.RemoveSessionFromToken(token)
if err != nil {
if err == sql.ErrNoRows {
return fmt.Errorf("token not found")
}
return fmt.Errorf("failed to remove account session")
}
return nil
}
-28
View File
@@ -1,28 +0,0 @@
package account
import (
"regexp"
"golang.org/x/crypto/argon2"
)
type GenericAuthResponse struct {
Token string `json:"token"`
}
const (
ArgonTime = 1
ArgonMemory = 256 * 1024
ArgonThreads = 4
ArgonKeySize = 32
ArgonSaltSize = 16
UUIDSize = 16
TokenSize = 32
)
var isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString
func deriveArgon2IDKey(password, salt []byte) []byte {
return argon2.IDKey(password, salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize)
}
-39
View File
@@ -1,39 +0,0 @@
package account
import (
"fmt"
"os"
"strconv"
"time"
"github.com/pagefaultgames/pokerogue-server/defs"
)
type InfoResponse struct {
Username string `json:"username"`
LastSessionSlot int `json:"lastSessionSlot"`
}
// /account/info - get account info
func Info(username string, uuid []byte) (InfoResponse, error) {
var latestSave time.Time
latestSaveID := -1
for id := range defs.SessionSlotCount {
fileName := "session"
if id != 0 {
fileName += strconv.Itoa(id)
}
stat, err := os.Stat(fmt.Sprintf("userdata/%x/%s.pzs", uuid, fileName))
if err != nil {
continue
}
if stat.ModTime().After(latestSave) {
latestSave = stat.ModTime()
latestSaveID = id
}
}
return InfoResponse{Username: username, LastSessionSlot: latestSaveID}, nil
}
-54
View File
@@ -1,54 +0,0 @@
package account
import (
"bytes"
"crypto/rand"
"database/sql"
"encoding/base64"
"fmt"
"github.com/pagefaultgames/pokerogue-server/db"
)
type LoginResponse GenericAuthResponse
// /account/login - log into account
func Login(username, password string) (LoginResponse, error) {
var response LoginResponse
if !isValidUsername(username) {
return response, fmt.Errorf("invalid username")
}
if len(password) < 6 {
return response, fmt.Errorf("invalid password")
}
key, salt, err := db.FetchAccountKeySaltFromUsername(username)
if err != nil {
if err == sql.ErrNoRows {
return response, fmt.Errorf("account doesn't exist")
}
return response, err
}
if !bytes.Equal(key, deriveArgon2IDKey([]byte(password), salt)) {
return response, fmt.Errorf("password doesn't match")
}
token := make([]byte, TokenSize)
_, err = rand.Read(token)
if err != nil {
return response, fmt.Errorf("failed to generate token: %s", err)
}
err = db.AddAccountSession(username, token)
if err != nil {
return response, fmt.Errorf("failed to add account session")
}
response.Token = base64.StdEncoding.EncodeToString(token)
return response, nil
}
-26
View File
@@ -1,26 +0,0 @@
package account
import (
"database/sql"
"fmt"
"github.com/pagefaultgames/pokerogue-server/db"
)
// /account/logout - log out of account
func Logout(token []byte) error {
if len(token) != TokenSize {
return fmt.Errorf("invalid token")
}
err := db.RemoveSessionFromToken(token)
if err != nil {
if err == sql.ErrNoRows {
return fmt.Errorf("token not found")
}
return fmt.Errorf("failed to remove account session")
}
return nil
}
-38
View File
@@ -1,38 +0,0 @@
package account
import (
"crypto/rand"
"fmt"
"github.com/pagefaultgames/pokerogue-server/db"
)
// /account/register - register account
func Register(username, password string) error {
if !isValidUsername(username) {
return fmt.Errorf("invalid username")
}
if len(password) < 6 {
return fmt.Errorf("invalid password")
}
uuid := make([]byte, UUIDSize)
_, err := rand.Read(uuid)
if err != nil {
return fmt.Errorf("failed to generate uuid: %s", err)
}
salt := make([]byte, ArgonSaltSize)
_, err = rand.Read(salt)
if err != nil {
return fmt.Errorf(fmt.Sprintf("failed to generate salt: %s", err))
}
err = db.AddAccountRecord(uuid, username, deriveArgon2IDKey([]byte(password), salt), salt)
if err != nil {
return fmt.Errorf("failed to add account record: %s", err)
}
return nil
}
-59
View File
@@ -1,59 +0,0 @@
package api
import (
"encoding/base64"
"fmt"
"github.com/pagefaultgames/pokerogue-server/api/account"
"github.com/pagefaultgames/pokerogue-server/api/daily"
"github.com/pagefaultgames/pokerogue-server/db"
)
func Init() {
scheduleStatRefresh()
daily.Init()
}
func usernameFromTokenHeader(token string) (string, error) {
if token == "" {
return "", fmt.Errorf("missing token")
}
decoded, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return "", fmt.Errorf("failed to decode token: %s", err)
}
if len(decoded) != account.TokenSize {
return "", fmt.Errorf("invalid token length: got %d, expected %d", len(token), account.TokenSize)
}
username, err := db.FetchUsernameFromToken(decoded)
if err != nil {
return "", fmt.Errorf("failed to validate token: %s", err)
}
return username, nil
}
func uuidFromTokenHeader(token string) ([]byte, error) {
if token == "" {
return nil, fmt.Errorf("missing token")
}
decoded, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return nil, fmt.Errorf("failed to decode token: %s", err)
}
if len(decoded) != account.TokenSize {
return nil, fmt.Errorf("invalid token length: got %d, expected %d", len(token), account.TokenSize)
}
uuid, err := db.FetchUUIDFromToken(decoded)
if err != nil {
return nil, fmt.Errorf("failed to validate token: %s", err)
}
return uuid, nil
}
+105
View File
@@ -0,0 +1,105 @@
package api
import (
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"fmt"
"log"
"os"
"time"
"github.com/Flashfyre/pokerogue-server/db"
"github.com/Flashfyre/pokerogue-server/defs"
"github.com/go-co-op/gocron"
)
const secondsPerDay = 60 * 60 * 24
var (
dailyRunScheduler = gocron.NewScheduler(time.UTC)
dailyRunSecret []byte
dailyRunSeed string
)
func ScheduleDailyRunRefresh() {
dailyRunScheduler.Every(1).Day().At("00:00").Do(func() error {
err := InitDailyRun()
if err != nil {
log.Fatal(err)
}
return nil
}())
dailyRunScheduler.StartAsync()
}
func InitDailyRun() error {
secret, err := os.ReadFile("secret.key")
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to read daily seed secret: %s", err)
}
newSecret := make([]byte, 32)
_, err := rand.Read(newSecret)
if err != nil {
return fmt.Errorf("failed to generate daily seed secret: %s", err)
}
err = os.WriteFile("secret.key", newSecret, 0400)
if err != nil {
return fmt.Errorf("failed to write daily seed secret: %s", err)
}
secret = newSecret
}
dailyRunSecret = secret
dailyRunSeed = base64.StdEncoding.EncodeToString(deriveDailyRunSeed(time.Now().UTC()))
err = db.TryAddDailyRun(dailyRunSeed)
if err != nil {
log.Print(err)
}
log.Printf("Daily Run Seed: %s", dailyRunSeed)
return nil
}
func deriveDailyRunSeed(seedTime time.Time) []byte {
day := make([]byte, 8)
binary.BigEndian.PutUint64(day, uint64(seedTime.Unix()/secondsPerDay))
hashedSeed := md5.Sum(append(day, dailyRunSecret...))
return hashedSeed[:]
}
// /daily/rankings - fetch daily rankings
func handleRankings(uuid []byte, category, page int) ([]defs.DailyRanking, error) {
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
rankings, err := db.FetchRankings(category, page)
if err != nil {
log.Print("failed to retrieve rankings")
}
return rankings, nil
}
// /daily/rankingpagecount - fetch daily ranking page count
func handleRankingPageCount(category int) (int, error) {
pageCount, err := db.FetchRankingPageCount(category)
if err != nil {
log.Print("failed to retrieve ranking page count")
}
return pageCount, nil
}
-88
View File
@@ -1,88 +0,0 @@
package daily
import (
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"fmt"
"log"
"os"
"time"
"github.com/pagefaultgames/pokerogue-server/db"
"github.com/robfig/cron/v3"
)
const secondsPerDay = 60 * 60 * 24
var (
scheduler = cron.New(cron.WithLocation(time.UTC))
secret []byte
)
func Init() error {
var err error
secret, err = os.ReadFile("secret.key")
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to read daily seed secret: %s", err)
}
newSecret := make([]byte, 32)
_, err := rand.Read(newSecret)
if err != nil {
return fmt.Errorf("failed to generate daily seed secret: %s", err)
}
err = os.WriteFile("secret.key", newSecret, 0400)
if err != nil {
return fmt.Errorf("failed to write daily seed secret: %s", err)
}
secret = newSecret
}
err = recordNewDaily()
if err != nil {
log.Print(err)
}
log.Printf("Daily Run Seed: %s", Seed())
scheduler.AddFunc("@daily", func() {
time.Sleep(time.Second)
err := recordNewDaily()
if err != nil {
log.Printf("error while recording new daily: %s", err)
}
})
scheduler.Start()
return nil
}
func Seed() string {
return base64.StdEncoding.EncodeToString(deriveSeed(time.Now().UTC()))
}
func deriveSeed(seedTime time.Time) []byte {
day := make([]byte, 8)
binary.BigEndian.PutUint64(day, uint64(seedTime.Unix()/secondsPerDay))
hashedSeed := md5.Sum(append(day, secret...))
return hashedSeed[:]
}
func recordNewDaily() error {
err := db.TryAddDailyRun(Seed())
if err != nil {
return err
}
return nil
}
-23
View File
@@ -1,23 +0,0 @@
package daily
import (
"log"
"github.com/pagefaultgames/pokerogue-server/db"
"github.com/pagefaultgames/pokerogue-server/defs"
)
// /daily/rankings - fetch daily rankings
func Rankings(uuid []byte, category, page int) ([]defs.DailyRanking, error) {
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
rankings, err := db.FetchRankings(category, page)
if err != nil {
log.Print("failed to retrieve rankings")
}
return rankings, nil
}
-17
View File
@@ -1,17 +0,0 @@
package daily
import (
"log"
"github.com/pagefaultgames/pokerogue-server/db"
)
// /daily/rankingpagecount - fetch daily ranking page count
func RankingPageCount(category int) (int, error) {
pageCount, err := db.FetchRankingPageCount(category)
if err != nil {
log.Print("failed to retrieve ranking page count")
}
return pageCount, nil
}
-260
View File
@@ -1,260 +0,0 @@
package api
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/pagefaultgames/pokerogue-server/api/account"
"github.com/pagefaultgames/pokerogue-server/api/daily"
"github.com/pagefaultgames/pokerogue-server/api/savedata"
"github.com/pagefaultgames/pokerogue-server/defs"
"github.com/valyala/fasthttp"
)
/*
The caller of endpoint handler functions are responsible for extracting the necessary data from the request.
Handler functions are responsible for checking the validity of this data and returning a result or error.
Handlers should not return serialized JSON, instead return the struct itself.
*/
func HandleAccountInfo(ctx *fasthttp.RequestCtx) {
username, err := usernameFromTokenHeader(string(ctx.Request.Header.Peek("Authorization")))
if err != nil {
httpError(ctx, err, http.StatusBadRequest)
return
}
uuid, err := uuidFromTokenHeader(string(ctx.Request.Header.Peek("Authorization"))) // lazy
if err != nil {
httpError(ctx, err, http.StatusBadRequest)
return
}
response, err := account.Info(username, uuid)
if err != nil {
httpError(ctx, err, http.StatusInternalServerError)
return
}
err = json.NewEncoder(ctx.Response.BodyWriter()).Encode(response)
if err != nil {
httpError(ctx, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
return
}
}
func HandleAccountRegister(ctx *fasthttp.RequestCtx) {
err := account.Register(string(ctx.PostArgs().Peek("username")), string(ctx.PostArgs().Peek("password")))
if err != nil {
httpError(ctx, err, http.StatusInternalServerError)
return
}
ctx.SetStatusCode(http.StatusOK)
}
func HandleAccountLogin(ctx *fasthttp.RequestCtx) {
response, err := account.Login(string(ctx.PostArgs().Peek("username")), string(ctx.PostArgs().Peek("password")))
if err != nil {
httpError(ctx, err, http.StatusInternalServerError)
return
}
err = json.NewEncoder(ctx.Response.BodyWriter()).Encode(response)
if err != nil {
httpError(ctx, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
return
}
}
func HandleAccountLogout(ctx *fasthttp.RequestCtx) {
token, err := base64.StdEncoding.DecodeString(string(ctx.Request.Header.Peek("Authorization")))
if err != nil {
httpError(ctx, fmt.Errorf("failed to decode token: %s", err), http.StatusBadRequest)
return
}
err = account.Logout(token)
if err != nil {
httpError(ctx, err, http.StatusInternalServerError)
return
}
ctx.SetStatusCode(http.StatusOK)
}
func HandleGamePlayerCount(ctx *fasthttp.RequestCtx) {
ctx.SetBody([]byte(strconv.Itoa(playerCount)))
}
func HandleGameTitleStats(ctx *fasthttp.RequestCtx) {
err := json.NewEncoder(ctx.Response.BodyWriter()).Encode(defs.TitleStats{
PlayerCount: playerCount,
BattleCount: battleCount,
})
if err != nil {
httpError(ctx, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
return
}
}
func HandleGameClassicSessionCount(ctx *fasthttp.RequestCtx) {
ctx.SetBody([]byte(strconv.Itoa(classicSessionCount)))
}
func HandleSaveData(ctx *fasthttp.RequestCtx) {
uuid, err := uuidFromTokenHeader(string(ctx.Request.Header.Peek("Authorization")))
if err != nil {
httpError(ctx, err, http.StatusBadRequest)
return
}
datatype := -1
if ctx.QueryArgs().Has("datatype") {
datatype, err = strconv.Atoi(string(ctx.QueryArgs().Peek("datatype")))
if err != nil {
httpError(ctx, err, http.StatusBadRequest)
return
}
}
var slot int
if ctx.QueryArgs().Has("slot") {
slot, err = strconv.Atoi(string(ctx.QueryArgs().Peek("slot")))
if err != nil {
httpError(ctx, err, http.StatusBadRequest)
return
}
}
var save any
// /savedata/get and /savedata/delete specify datatype, but don't expect data in body
if string(ctx.Path()) != "/api/savedata/get" && string(ctx.Path()) != "/api/savedata/delete" {
if datatype == 0 {
var system defs.SystemSaveData
err = json.Unmarshal(ctx.Request.Body(), &system)
if err != nil {
httpError(ctx, fmt.Errorf("failed to unmarshal request body: %s", err), http.StatusBadRequest)
return
}
save = system
// /savedata/clear doesn't specify datatype, it is assumed to be 1 (session)
} else if datatype == 1 || string(ctx.Path()) == "/api/savedata/clear" {
var session defs.SessionSaveData
err = json.Unmarshal(ctx.Request.Body(), &session)
if err != nil {
httpError(ctx, fmt.Errorf("failed to unmarshal request body: %s", err), http.StatusBadRequest)
return
}
save = session
}
}
switch string(ctx.Path()) {
case "/api/savedata/get":
save, err = savedata.Get(uuid, datatype, slot)
case "/api/savedata/update":
err = savedata.Update(uuid, slot, save)
case "/api/savedata/delete":
err = savedata.Delete(uuid, datatype, slot)
case "/api/savedata/clear":
s, ok := save.(defs.SessionSaveData)
if !ok {
httpError(ctx, fmt.Errorf("save data is not type SessionSaveData"), http.StatusBadRequest)
return
}
// doesn't return a save, but it works
save, err = savedata.Clear(uuid, slot, daily.Seed(), s)
}
if err != nil {
httpError(ctx, err, http.StatusInternalServerError)
return
}
if save == nil || string(ctx.Path()) == "/api/savedata/update" {
ctx.SetStatusCode(http.StatusOK)
return
}
err = json.NewEncoder(ctx.Response.BodyWriter()).Encode(save)
if err != nil {
httpError(ctx, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
return
}
}
func HandleDailySeed(ctx *fasthttp.RequestCtx) {
ctx.Response.SetBody([]byte(daily.Seed()))
}
func HandleDailyRankings(ctx *fasthttp.RequestCtx) {
uuid, err := uuidFromTokenHeader(string(ctx.Request.Header.Peek("Authorization")))
if err != nil {
httpError(ctx, err, http.StatusBadRequest)
return
}
var category int
if ctx.QueryArgs().Has("category") {
category, err = strconv.Atoi(string(ctx.QueryArgs().Peek("category")))
if err != nil {
httpError(ctx, fmt.Errorf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
page := 1
if ctx.QueryArgs().Has("page") {
page, err = strconv.Atoi(string(ctx.QueryArgs().Peek("page")))
if err != nil {
httpError(ctx, fmt.Errorf("failed to convert page: %s", err), http.StatusBadRequest)
return
}
}
rankings, err := daily.Rankings(uuid, category, page)
if err != nil {
httpError(ctx, err, http.StatusInternalServerError)
return
}
err = json.NewEncoder(ctx.Response.BodyWriter()).Encode(rankings)
if err != nil {
httpError(ctx, fmt.Errorf("failed to encode response json: %s", err), http.StatusInternalServerError)
return
}
}
func HandleDailyRankingPageCount(ctx *fasthttp.RequestCtx) {
var category int
if ctx.QueryArgs().Has("category") {
var err error
category, err = strconv.Atoi(string(ctx.QueryArgs().Peek("category")))
if err != nil {
httpError(ctx, fmt.Errorf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
count, err := daily.RankingPageCount(category)
if err != nil {
httpError(ctx, err, http.StatusInternalServerError)
return
}
ctx.SetBody([]byte(strconv.Itoa(count)))
}
func httpError(ctx *fasthttp.RequestCtx, err error, code int) {
log.Printf("%s: %s\n", ctx.Path(), err)
ctx.Error(err.Error(), code)
}
+11
View File
@@ -0,0 +1,11 @@
package api
import (
"log"
"net/http"
)
func httpError(w http.ResponseWriter, r *http.Request, err error, code int) {
log.Printf("%s: %s\n", r.URL.Path, err)
http.Error(w, err.Error(), code)
}
+39
View File
@@ -0,0 +1,39 @@
package api
import (
"log"
"time"
"github.com/Flashfyre/pokerogue-server/db"
"github.com/go-co-op/gocron"
)
var (
statScheduler = gocron.NewScheduler(time.UTC)
playerCount int
battleCount int
classicSessionCount int
)
func ScheduleStatRefresh() {
statScheduler.Every(10).Second().Do(updateStats)
statScheduler.StartAsync()
}
func updateStats() {
var err error
playerCount, err = db.FetchPlayerCount()
if err != nil {
log.Print(err)
}
battleCount, err = db.FetchBattleCount()
if err != nil {
log.Print(err)
}
classicSessionCount, err = db.FetchClassicSessionCount()
if err != nil {
log.Print(err)
}
}
+285
View File
@@ -0,0 +1,285 @@
package api
import (
"encoding/base64"
"encoding/gob"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/Flashfyre/pokerogue-server/defs"
)
type Server struct {
Debug bool
}
/*
The caller of endpoint handler functions are responsible for extracting the necessary data from the request.
Handler functions are responsible for checking the validity of this data and returning a result or error.
Handlers should not return serialized JSON, instead return the struct itself.
*/
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
gob.Register([]interface{}{})
gob.Register(map[string]interface{}{})
if s.Debug {
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Origin", "*")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
}
switch r.URL.Path {
// /account
case "/account/info":
username, err := getUsernameFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
uuid, err := getUUIDFromRequest(r) // lazy
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
info, err := handleAccountInfo(username, uuid)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
response, err := json.Marshal(info)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/account/register":
var request AccountRegisterRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
err = handleAccountRegister(request.Username, request.Password)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
case "/account/login":
var request AccountLoginRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
token, err := handleAccountLogin(request.Username, request.Password)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
response, err := json.Marshal(token)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/account/logout":
token, err := base64.StdEncoding.DecodeString(r.Header.Get("Authorization"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode token: %s", err), http.StatusBadRequest)
return
}
err = handleAccountLogout(token)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
// /game
case "/game/playercount":
w.Write([]byte(strconv.Itoa(playerCount)))
case "/game/titlestats":
response, err := json.Marshal(&defs.TitleStats{
PlayerCount: playerCount,
BattleCount: battleCount,
})
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/game/classicsessioncount":
w.Write([]byte(strconv.Itoa(classicSessionCount)))
// /savedata
case "/savedata/get", "/savedata/update", "/savedata/delete", "/savedata/clear":
uuid, err := getUUIDFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
datatype := -1
if r.URL.Query().Has("datatype") {
datatype, err = strconv.Atoi(r.URL.Query().Get("datatype"))
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
}
var slot int
if r.URL.Query().Has("slot") {
slot, err = strconv.Atoi(r.URL.Query().Get("slot"))
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
}
var save any
// /savedata/get and /savedata/delete specify datatype, but don't expect data in body
if r.URL.Path != "/savedata/get" && r.URL.Path != "/savedata/delete" {
if datatype == 0 {
var system defs.SystemSaveData
err = json.NewDecoder(r.Body).Decode(&system)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
save = system
// /savedata/clear doesn't specify datatype, it is assumed to be 1 (session)
} else if datatype == 1 || r.URL.Path == "/savedata/clear" {
var session defs.SessionSaveData
err = json.NewDecoder(r.Body).Decode(&session)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
save = session
}
}
switch r.URL.Path {
case "/savedata/get":
save, err = handleSavedataGet(uuid, datatype, slot)
case "/savedata/update":
err = handleSavedataUpdate(uuid, slot, save)
case "/savedata/delete":
err = handleSavedataDelete(uuid, datatype, slot)
case "/savedata/clear":
// doesn't return a save, but it works
save, err = handleSavedataClear(uuid, slot, save.(defs.SessionSaveData))
}
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
if save == nil || r.URL.Path == "/savedata/update" {
w.WriteHeader(http.StatusOK)
return
}
response, err := json.Marshal(save)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
// /daily
case "/daily/seed":
w.Write([]byte(dailyRunSeed))
case "/daily/rankings":
uuid, err := getUUIDFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
var category int
if r.URL.Query().Has("category") {
category, err = strconv.Atoi(r.URL.Query().Get("category"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
page := 1
if r.URL.Query().Has("page") {
page, err = strconv.Atoi(r.URL.Query().Get("page"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to convert page: %s", err), http.StatusBadRequest)
return
}
}
rankings, err := handleRankings(uuid, category, page)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
response, err := json.Marshal(rankings)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/daily/rankingpagecount":
var category int
if r.URL.Query().Has("category") {
var err error
category, err = strconv.Atoi(r.URL.Query().Get("category"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
count, err := handleRankingPageCount(category)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
}
w.Write([]byte(strconv.Itoa(count)))
}
}
// auth
type GenericAuthRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type GenericAuthResponse struct {
Token string `json:"token"`
}
+85
View File
@@ -0,0 +1,85 @@
package api
import (
"bytes"
"encoding/gob"
"encoding/hex"
"fmt"
"os"
"strconv"
"github.com/Flashfyre/pokerogue-server/defs"
"github.com/klauspost/compress/zstd"
)
func readSystemSaveData(uuid []byte) (defs.SystemSaveData, error) {
var system defs.SystemSaveData
save, err := os.ReadFile("userdata/" + hex.EncodeToString(uuid) + "/system.pzs")
if err != nil {
return system, fmt.Errorf("failed to read save file: %s", err)
}
zstdReader, err := zstd.NewReader(nil)
if err != nil {
return system, fmt.Errorf("failed to create zstd reader: %s", err)
}
decompressed, err := zstdReader.DecodeAll(save, nil)
if err != nil {
return system, fmt.Errorf("failed to decompress save file: %s", err)
}
gobDecoderBuf := bytes.NewBuffer(decompressed)
err = gob.NewDecoder(gobDecoderBuf).Decode(&system)
if err != nil {
return system, fmt.Errorf("failed to deserialize save: %s", err)
}
return system, nil
}
func readSessionSaveData(uuid []byte, slotID int) (defs.SessionSaveData, error) {
var session defs.SessionSaveData
fileName := "session"
if slotID != 0 {
fileName += strconv.Itoa(slotID)
}
save, err := os.ReadFile(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
if err != nil {
return session, fmt.Errorf("failed to read save file: %s", err)
}
zstdReader, err := zstd.NewReader(nil)
if err != nil {
return session, fmt.Errorf("failed to create zstd reader: %s", err)
}
decompressed, err := zstdReader.DecodeAll(save, nil)
if err != nil {
return session, fmt.Errorf("failed to decompress save file: %s", err)
}
gobDecoderBuf := bytes.NewBuffer(decompressed)
err = gob.NewDecoder(gobDecoderBuf).Decode(&session)
if err != nil {
return session, fmt.Errorf("failed to deserialize save: %s", err)
}
return session, nil
}
func validateSessionCompleted(session defs.SessionSaveData) bool {
switch session.GameMode {
case 0:
return session.BattleType == 2 && session.WaveIndex == 200
case 3:
return session.BattleType == 2 && session.WaveIndex == 50
}
return false
}
+221
View File
@@ -0,0 +1,221 @@
package api
import (
"bytes"
"encoding/gob"
"encoding/hex"
"fmt"
"log"
"os"
"strconv"
"github.com/Flashfyre/pokerogue-server/db"
"github.com/Flashfyre/pokerogue-server/defs"
"github.com/klauspost/compress/zstd"
)
const sessionSlotCount = 3
// /savedata/get - get save data
func handleSavedataGet(uuid []byte, datatype, slot int) (any, error) {
switch datatype {
case 0: // System
system, err := readSystemSaveData(uuid)
if err != nil {
return nil, err
}
compensations, err := db.FetchAndClaimAccountCompensations(uuid)
if err != nil {
return nil, fmt.Errorf("failed to fetch compensations: %s", err)
}
for k, v := range compensations {
typeKey := strconv.Itoa(k)
system.VoucherCounts[typeKey] += v
}
return system, nil
case 1: // Session
if slot < 0 || slot >= sessionSlotCount {
return nil, fmt.Errorf("slot id %d out of range", slot)
}
session, err := readSessionSaveData(uuid, slot)
if err != nil {
return nil, err
}
return session, nil
default:
return nil, fmt.Errorf("invalid data type")
}
}
// /savedata/update - update save data
func handleSavedataUpdate(uuid []byte, slot int, save any) error {
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
hexUUID := hex.EncodeToString(uuid)
switch save := save.(type) {
case defs.SystemSaveData: // System
if save.TrainerId == 0 && save.SecretId == 0 {
return fmt.Errorf("invalid system data")
}
err = db.UpdateAccountStats(uuid, save.GameStats)
if err != nil {
return fmt.Errorf("failed to update account stats: %s", err)
}
var gobBuffer bytes.Buffer
err = gob.NewEncoder(&gobBuffer).Encode(save)
if err != nil {
return fmt.Errorf("failed to serialize save: %s", err)
}
zstdWriter, err := zstd.NewWriter(nil)
if err != nil {
return fmt.Errorf("failed to create zstd writer, %s", err)
}
compressed := zstdWriter.EncodeAll(gobBuffer.Bytes(), nil)
err = os.MkdirAll("userdata/"+hexUUID, 0755)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create userdata folder: %s", err)
}
err = os.WriteFile("userdata/"+hexUUID+"/system.pzs", compressed, 0644)
if err != nil {
return fmt.Errorf("failed to write save file: %s", err)
}
db.DeleteClaimedAccountCompensations(uuid)
case defs.SessionSaveData: // Session
if slot < 0 || slot >= sessionSlotCount {
return fmt.Errorf("slot id %d out of range", slot)
}
fileName := "session"
if slot != 0 {
fileName += strconv.Itoa(slot)
}
var gobBuffer bytes.Buffer
err = gob.NewEncoder(&gobBuffer).Encode(save)
if err != nil {
return fmt.Errorf("failed to serialize save: %s", err)
}
zstdWriter, err := zstd.NewWriter(nil)
if err != nil {
return fmt.Errorf("failed to create zstd writer, %s", err)
}
compressed := zstdWriter.EncodeAll(gobBuffer.Bytes(), nil)
err = os.MkdirAll("userdata/"+hexUUID, 0755)
if err != nil && !os.IsExist(err) {
return fmt.Errorf(fmt.Sprintf("failed to create userdata folder: %s", err))
}
err = os.WriteFile(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName), compressed, 0644)
if err != nil {
return fmt.Errorf("failed to write save file: %s", err)
}
default:
return fmt.Errorf("invalid data type")
}
return nil
}
// /savedata/delete - delete save data
func handleSavedataDelete(uuid []byte, datatype, slot int) error {
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
hexUUID := hex.EncodeToString(uuid)
switch datatype {
case 0: // System
err := os.Remove("userdata/" + hexUUID + "/system.pzs")
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete save file: %s", err)
}
case 1: // Session
if slot < 0 || slot >= sessionSlotCount {
return fmt.Errorf("slot id %d out of range", slot)
}
fileName := "session"
if slot != 0 {
fileName += strconv.Itoa(slot)
}
err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName))
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete save file: %s", err)
}
default:
return fmt.Errorf("invalid data type")
}
return nil
}
type SavedataClearResponse struct {
Success bool `json:"success"`
}
// /savedata/clear - mark session save data as cleared and delete
func handleSavedataClear(uuid []byte, slot int, save defs.SessionSaveData) (SavedataClearResponse, error) {
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
if slot < 0 || slot >= sessionSlotCount {
return SavedataClearResponse{}, fmt.Errorf("slot id %d out of range", slot)
}
sessionCompleted := validateSessionCompleted(save)
newCompletion := false
if save.GameMode == 3 && save.Seed == dailyRunSeed {
waveCompleted := save.WaveIndex
if !sessionCompleted {
waveCompleted--
}
err = db.AddOrUpdateAccountDailyRun(uuid, save.Score, waveCompleted)
if err != nil {
log.Printf("failed to add or update daily run record: %s", err)
}
}
if sessionCompleted {
newCompletion, err = db.TryAddSeedCompletion(uuid, save.Seed, int(save.GameMode))
if err != nil {
log.Printf("failed to mark seed as completed: %s", err)
}
}
fileName := "session"
if slot != 0 {
fileName += strconv.Itoa(slot)
}
err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
if err != nil && !os.IsNotExist(err) {
return SavedataClearResponse{}, fmt.Errorf("failed to delete save file: %s", err)
}
return SavedataClearResponse{Success: newCompletion}, nil
}
-62
View File
@@ -1,62 +0,0 @@
package savedata
import (
"encoding/hex"
"fmt"
"log"
"os"
"strconv"
"github.com/pagefaultgames/pokerogue-server/db"
"github.com/pagefaultgames/pokerogue-server/defs"
)
type ClearResponse struct {
Success bool `json:"success"`
}
// /savedata/clear - mark session save data as cleared and delete
func Clear(uuid []byte, slot int, seed string, save defs.SessionSaveData) (ClearResponse, error) {
var response ClearResponse
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
if slot < 0 || slot >= defs.SessionSlotCount {
return response, fmt.Errorf("slot id %d out of range", slot)
}
sessionCompleted := validateSessionCompleted(save)
if save.GameMode == 3 && save.Seed == seed {
waveCompleted := save.WaveIndex
if !sessionCompleted {
waveCompleted--
}
err = db.AddOrUpdateAccountDailyRun(uuid, save.Score, waveCompleted)
if err != nil {
log.Printf("failed to add or update daily run record: %s", err)
}
}
if sessionCompleted {
response.Success, err = db.TryAddSeedCompletion(uuid, save.Seed, int(save.GameMode))
if err != nil {
log.Printf("failed to mark seed as completed: %s", err)
}
}
fileName := "session"
if slot != 0 {
fileName += strconv.Itoa(slot)
}
err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
if err != nil && !os.IsNotExist(err) {
return response, fmt.Errorf("failed to delete save file: %s", err)
}
return response, nil
}
-78
View File
@@ -1,78 +0,0 @@
package savedata
import (
"encoding/gob"
"encoding/hex"
"fmt"
"os"
"strconv"
"github.com/klauspost/compress/zstd"
"github.com/pagefaultgames/pokerogue-server/defs"
)
func readSystemSaveData(uuid []byte) (defs.SystemSaveData, error) {
var system defs.SystemSaveData
file, err := os.Open("userdata/" + hex.EncodeToString(uuid) + "/system.pzs")
if err != nil {
return system, fmt.Errorf("failed to open save file for reading: %s", err)
}
defer file.Close()
zstdDecoder, err := zstd.NewReader(file)
if err != nil {
return system, fmt.Errorf("failed to create zstd decoder: %s", err)
}
defer zstdDecoder.Close()
err = gob.NewDecoder(zstdDecoder).Decode(&system)
if err != nil {
return system, fmt.Errorf("failed to deserialize save: %s", err)
}
return system, nil
}
func readSessionSaveData(uuid []byte, slotID int) (defs.SessionSaveData, error) {
var session defs.SessionSaveData
fileName := "session"
if slotID != 0 {
fileName += strconv.Itoa(slotID)
}
file, err := os.Open(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
if err != nil {
return session, fmt.Errorf("failed to open save file for reading: %s", err)
}
defer file.Close()
zstdDecoder, err := zstd.NewReader(file)
if err != nil {
return session, fmt.Errorf("failed to create zstd decoder: %s", err)
}
defer zstdDecoder.Close()
err = gob.NewDecoder(zstdDecoder).Decode(&session)
if err != nil {
return session, fmt.Errorf("failed to deserialize save: %s", err)
}
return session, nil
}
func validateSessionCompleted(session defs.SessionSaveData) bool {
switch session.GameMode {
case 0:
return session.BattleType == 2 && session.WaveIndex == 200
case 3:
return session.BattleType == 2 && session.WaveIndex == 50
}
return false
}
-48
View File
@@ -1,48 +0,0 @@
package savedata
import (
"encoding/hex"
"fmt"
"log"
"os"
"strconv"
"github.com/pagefaultgames/pokerogue-server/db"
"github.com/pagefaultgames/pokerogue-server/defs"
)
// /savedata/delete - delete save data
func Delete(uuid []byte, datatype, slot int) error {
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
hexUUID := hex.EncodeToString(uuid)
switch datatype {
case 0: // System
err := os.Remove("userdata/" + hexUUID + "/system.pzs")
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete save file: %s", err)
}
case 1: // Session
if slot < 0 || slot >= defs.SessionSlotCount {
return fmt.Errorf("slot id %d out of range", slot)
}
fileName := "session"
if slot != 0 {
fileName += strconv.Itoa(slot)
}
err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName))
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete save file: %s", err)
}
default:
return fmt.Errorf("invalid data type")
}
return nil
}
-44
View File
@@ -1,44 +0,0 @@
package savedata
import (
"fmt"
"strconv"
"github.com/pagefaultgames/pokerogue-server/db"
"github.com/pagefaultgames/pokerogue-server/defs"
)
// /savedata/get - get save data
func Get(uuid []byte, datatype, slot int) (any, error) {
switch datatype {
case 0: // System
system, err := readSystemSaveData(uuid)
if err != nil {
return nil, err
}
compensations, err := db.FetchAndClaimAccountCompensations(uuid)
if err != nil {
return nil, fmt.Errorf("failed to fetch compensations: %s", err)
}
for compensationType, amount := range compensations {
system.VoucherCounts[strconv.Itoa(compensationType)] += amount
}
return system, nil
case 1: // Session
if slot < 0 || slot >= defs.SessionSlotCount {
return nil, fmt.Errorf("slot id %d out of range", slot)
}
session, err := readSessionSaveData(uuid, slot)
if err != nil {
return nil, err
}
return session, nil
default:
return nil, fmt.Errorf("invalid data type")
}
}
-103
View File
@@ -1,103 +0,0 @@
package savedata
import (
"encoding/gob"
"encoding/hex"
"fmt"
"log"
"os"
"strconv"
"github.com/klauspost/compress/zstd"
"github.com/pagefaultgames/pokerogue-server/db"
"github.com/pagefaultgames/pokerogue-server/defs"
)
// /savedata/update - update save data
func Update(uuid []byte, slot int, save any) error {
err := db.UpdateAccountLastActivity(uuid)
if err != nil {
log.Print("failed to update account last activity")
}
hexUUID := hex.EncodeToString(uuid)
switch save := save.(type) {
case defs.SystemSaveData: // System
if save.TrainerId == 0 && save.SecretId == 0 {
return fmt.Errorf("invalid system data")
}
if save.GameVersion != "1.0.2" {
return fmt.Errorf("client version out of date")
}
err = db.UpdateAccountStats(uuid, save.GameStats, save.VoucherCounts)
if err != nil {
return fmt.Errorf("failed to update account stats: %s", err)
}
err = os.MkdirAll("userdata/"+hexUUID, 0755)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create userdata folder: %s", err)
}
file, err := os.OpenFile("userdata/"+hexUUID+"/system.pzs", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open save file for writing: %s", err)
}
defer file.Close()
zstdEncoder, err := zstd.NewWriter(file)
if err != nil {
return fmt.Errorf("failed to create zstd encoder: %s", err)
}
defer zstdEncoder.Close()
err = gob.NewEncoder(zstdEncoder).Encode(save)
if err != nil {
return fmt.Errorf("failed to serialize save: %s", err)
}
db.DeleteClaimedAccountCompensations(uuid)
case defs.SessionSaveData: // Session
if slot < 0 || slot >= defs.SessionSlotCount {
return fmt.Errorf("slot id %d out of range", slot)
}
fileName := "session"
if slot != 0 {
fileName += strconv.Itoa(slot)
}
err = os.MkdirAll("userdata/"+hexUUID, 0755)
if err != nil && !os.IsExist(err) {
return fmt.Errorf(fmt.Sprintf("failed to create userdata folder: %s", err))
}
file, err := os.OpenFile(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open save file for writing: %s", err)
}
defer file.Close()
zstdEncoder, err := zstd.NewWriter(file)
if err != nil {
return fmt.Errorf("failed to create zstd encoder: %s", err)
}
defer zstdEncoder.Close()
err = gob.NewEncoder(zstdEncoder).Encode(save)
if err != nil {
return fmt.Errorf("failed to serialize save: %s", err)
}
default:
return fmt.Errorf("invalid data type")
}
return nil
}
-47
View File
@@ -1,47 +0,0 @@
package api
import (
"log"
"time"
"github.com/pagefaultgames/pokerogue-server/db"
"github.com/robfig/cron/v3"
)
var (
scheduler = cron.New(cron.WithLocation(time.UTC))
playerCount int
battleCount int
classicSessionCount int
)
func scheduleStatRefresh() {
scheduler.AddFunc("@every 10s", func() {
err := updateStats()
if err != nil {
log.Printf("failed to update stats: %s", err)
}
})
scheduler.Start()
}
func updateStats() error {
var err error
playerCount, err = db.FetchPlayerCount()
if err != nil {
return err
}
battleCount, err = db.FetchBattleCount()
if err != nil {
return err
}
classicSessionCount, err = db.FetchClassicSessionCount()
if err != nil {
return err
}
return nil
}
+3 -21
View File
@@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"slices" "slices"
"github.com/Flashfyre/pokerogue-server/defs"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/pagefaultgames/pokerogue-server/defs"
) )
func AddAccountRecord(uuid []byte, username string, key, salt []byte) error { func AddAccountRecord(uuid []byte, username string, key, salt []byte) error {
@@ -41,8 +41,8 @@ func UpdateAccountLastActivity(uuid []byte) error {
return nil return nil
} }
func UpdateAccountStats(uuid []byte, stats defs.GameStats, voucherCounts map[string]int) error { func UpdateAccountStats(uuid []byte, stats defs.GameStats) error {
var columns = []string{"playTime", "battles", "classicSessionsPlayed", "sessionsWon", "highestEndlessWave", "highestLevel", "pokemonSeen", "pokemonDefeated", "pokemonCaught", "pokemonHatched", "eggsPulled", "regularVouchers", "plusVouchers", "premiumVouchers", "goldenVouchers"} var columns = []string{"playTime", "battles", "classicSessionsPlayed", "sessionsWon", "highestEndlessWave", "highestLevel", "pokemonSeen", "pokemonDefeated", "pokemonCaught", "pokemonHatched", "eggsPulled"}
var statCols []string var statCols []string
var statValues []interface{} var statValues []interface{}
@@ -64,24 +64,6 @@ func UpdateAccountStats(uuid []byte, stats defs.GameStats, voucherCounts map[str
} }
} }
for k, v := range voucherCounts {
var column string
switch k {
case "0":
column = "regularVouchers"
case "1":
column = "plusVouchers"
case "2":
column = "premiumVouchers"
case "3":
column = "goldenVouchers"
default:
continue
}
statCols = append(statCols, column)
statValues = append(statValues, v)
}
var statArgs []interface{} var statArgs []interface{}
statArgs = append(statArgs, uuid) statArgs = append(statArgs, uuid)
for range 2 { for range 2 {
+1 -1
View File
@@ -3,7 +3,7 @@ package db
import ( import (
"math" "math"
"github.com/pagefaultgames/pokerogue-server/defs" "github.com/Flashfyre/pokerogue-server/defs"
) )
func TryAddDailyRun(seed string) error { func TryAddDailyRun(seed string) error {
+2 -2
View File
@@ -12,7 +12,7 @@ func FetchPlayerCount() (int, error) {
func FetchBattleCount() (int, error) { func FetchBattleCount() (int, error) {
var battleCount int var battleCount int
err := handle.QueryRow("SELECT COALESCE(SUM(battles), 0) FROM accountStats").Scan(&battleCount) err := handle.QueryRow("SELECT SUM(battles) FROM accountStats").Scan(&battleCount)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -22,7 +22,7 @@ func FetchBattleCount() (int, error) {
func FetchClassicSessionCount() (int, error) { func FetchClassicSessionCount() (int, error) {
var classicSessionCount int var classicSessionCount int
err := handle.QueryRow("SELECT COALESCE(SUM(classicSessionsPlayed), 0) FROM accountStats").Scan(&classicSessionCount) err := handle.QueryRow("SELECT SUM(classicSessionsPlayed) FROM accountStats").Scan(&classicSessionCount)
if err != nil { if err != nil {
return 0, err return 0, err
} }
+6
View File
@@ -1,6 +1,12 @@
package db package db
func TryAddSeedCompletion(uuid []byte, seed string, mode int) (bool, error) { func TryAddSeedCompletion(uuid []byte, seed string, mode int) (bool, error) {
if len(seed) < 24 {
for range 24 - len(seed) {
seed += "0"
}
}
var count int var count int
err := handle.QueryRow("SELECT COUNT(*) FROM seedCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count) err := handle.QueryRow("SELECT COUNT(*) FROM seedCompletions WHERE uuid = ? AND seed = ?", uuid, seed).Scan(&count)
if err != nil { if err != nil {
+2 -16
View File
@@ -1,15 +1,12 @@
package defs package defs
const SessionSlotCount = 3
type SystemSaveData struct { type SystemSaveData struct {
TrainerId int `json:"trainerId"` TrainerId int `json:"trainerId"`
SecretId int `json:"secretId"` SecretId int `json:"secretId"`
Gender int `json:"gender"` Gender int `json:"gender"`
DexData DexData `json:"dexData"` DexData DexData `json:"dexData"`
StarterData StarterData `json:"starterData"` StarterMoveData StarterMoveData `json:"starterMoveData"`
StarterMoveData StarterMoveData `json:"starterMoveData"` // Legacy StarterEggMoveData StarterEggMoveData `json:"starterEggMoveData"`
StarterEggMoveData StarterEggMoveData `json:"starterEggMoveData"` // Legacy
GameStats GameStats `json:"gameStats"` GameStats GameStats `json:"gameStats"`
Unlocks Unlocks `json:"unlocks"` Unlocks Unlocks `json:"unlocks"`
AchvUnlocks AchvUnlocks `json:"achvUnlocks"` AchvUnlocks AchvUnlocks `json:"achvUnlocks"`
@@ -32,17 +29,6 @@ type DexEntry struct {
Ivs []int `json:"ivs"` Ivs []int `json:"ivs"`
} }
type StarterData map[int]StarterEntry
type StarterEntry struct {
Moveset interface{} `json:"moveset"`
EggMoves int `json:"eggMoves"`
CandyCount int `json:"candyCount"`
AbilityAttr int `json:"abilityAttr"`
PassiveAttr int `json:"passiveAttr"`
ValueReduction int `json:"valueReduction"`
}
type StarterMoveData map[int]interface{} type StarterMoveData map[int]interface{}
type StarterEggMoveData map[int]int type StarterEggMoveData map[int]int
+8 -8
View File
@@ -1,17 +1,17 @@
module github.com/pagefaultgames/pokerogue-server module github.com/Flashfyre/pokerogue-server
go 1.22 go 1.22
require ( require (
github.com/go-co-op/gocron v1.37.0
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/klauspost/compress v1.17.6 github.com/klauspost/compress v1.17.4
github.com/robfig/cron/v3 v3.0.1 golang.org/x/crypto v0.16.0
github.com/valyala/fasthttp v1.52.0
golang.org/x/crypto v0.19.0
) )
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/google/uuid v1.4.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
golang.org/x/sys v0.17.0 // indirect go.uber.org/atomic v1.9.0 // indirect
golang.org/x/sys v0.15.0 // indirect
) )
+42 -12
View File
@@ -1,16 +1,46 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+26 -62
View File
@@ -1,24 +1,21 @@
package main package main
import ( import (
"encoding/gob"
"flag" "flag"
"log" "log"
"strings" "net"
"net/http"
"os"
"github.com/pagefaultgames/pokerogue-server/api" "github.com/Flashfyre/pokerogue-server/api"
"github.com/pagefaultgames/pokerogue-server/db" "github.com/Flashfyre/pokerogue-server/db"
"github.com/valyala/fasthttp"
) )
var serveStaticContent fasthttp.RequestHandler
func main() { func main() {
// flag stuff debug := flag.Bool("debug", false, "debug mode")
addr := flag.String("addr", "0.0.0.0:80", "network address for api to listen on")
wwwpath := flag.String("wwwpath", "www", "path to static content to serve") proto := flag.String("proto", "tcp", "protocol for api to use (tcp, unix)")
tlscert := flag.String("tlscert", "", "path to tls certificate to use for https") addr := flag.String("addr", "0.0.0.0", "network address for api to listen on")
tlskey := flag.String("tlskey", "", "path to tls private key to use for https")
dbuser := flag.String("dbuser", "pokerogue", "database username") dbuser := flag.String("dbuser", "pokerogue", "database username")
dbpass := flag.String("dbpass", "", "database password") dbpass := flag.String("dbpass", "", "database password")
@@ -28,63 +25,30 @@ func main() {
flag.Parse() flag.Parse()
// register gob types
gob.Register([]interface{}{})
gob.Register(map[string]interface{}{})
// get database connection
err := db.Init(*dbuser, *dbpass, *dbproto, *dbaddr, *dbname) err := db.Init(*dbuser, *dbpass, *dbproto, *dbaddr, *dbname)
if err != nil { if err != nil {
log.Fatalf("failed to initialize database: %s", err) log.Fatalf("failed to initialize database: %s", err)
} }
// start web server if *proto == "unix" {
serveStaticContent = fasthttp.FSHandler(*wwwpath, 0) os.Remove(*addr)
api.Init()
if *tlscert != "" && *tlskey != "" {
err = fasthttp.ListenAndServeTLS(*addr, *tlscert, *tlskey, serve)
} else {
err = fasthttp.ListenAndServe(*addr, serve)
} }
listener, err := net.Listen(*proto, *addr)
if err != nil {
log.Fatalf("failed to create net listener: %s", err)
}
if *proto == "unix" {
os.Chmod(*addr, 0777)
}
api.ScheduleStatRefresh()
api.ScheduleDailyRunRefresh()
api.InitDailyRun()
err = http.Serve(listener, &api.Server{Debug: *debug})
if err != nil { if err != nil {
log.Fatalf("failed to create http server or server errored: %s", err) log.Fatalf("failed to create http server or server errored: %s", err)
} }
} }
func serve(ctx *fasthttp.RequestCtx) {
if strings.HasPrefix(string(ctx.Path()), "/api") {
switch string(ctx.Path()) {
case "/api/account/info":
api.HandleAccountInfo(ctx)
case "/api/account/register":
api.HandleAccountRegister(ctx)
case "/api/account/login":
api.HandleAccountLogin(ctx)
case "/api/account/logout":
api.HandleAccountLogout(ctx)
case "/api/game/playercount":
api.HandleGamePlayerCount(ctx)
case "/api/game/titlestats":
api.HandleGameTitleStats(ctx)
case "/api/game/classicsessioncount":
api.HandleGameClassicSessionCount(ctx)
case "/api/savedata/get", "/api/savedata/update", "/api/savedata/delete", "/api/savedata/clear":
api.HandleSaveData(ctx)
case "/api/daily/seed":
api.HandleDailySeed(ctx)
case "/api/daily/rankings":
api.HandleDailyRankings(ctx)
case "/api/daily/rankingpagecount":
api.HandleDailyRankingPageCount(ctx)
}
return
}
serveStaticContent(ctx)
}