Move endpoint categories into their own packages
parent
522ce9f4fa
commit
1f0f38d38e
141
api/account.go
141
api/account.go
|
@ -1,141 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pagefaultgames/pokerogue-server/db"
|
||||
)
|
||||
|
||||
const (
|
||||
UUIDSize = 16
|
||||
TokenSize = 32
|
||||
)
|
||||
|
||||
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(request AccountRegisterRequest) error {
|
||||
if !isValidUsername(request.Username) {
|
||||
return fmt.Errorf("invalid username")
|
||||
}
|
||||
|
||||
if len(request.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, request.Username, deriveArgon2IDKey([]byte(request.Password), salt), 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(request AccountLoginRequest) (AccountLoginResponse, error) {
|
||||
if !isValidUsername(request.Username) {
|
||||
return AccountLoginResponse{}, fmt.Errorf("invalid username")
|
||||
}
|
||||
|
||||
if len(request.Password) < 6 {
|
||||
return AccountLoginResponse{}, fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
key, salt, err := db.FetchAccountKeySaltFromUsername(request.Username)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return AccountLoginResponse{}, fmt.Errorf("account doesn't exist")
|
||||
}
|
||||
|
||||
return AccountLoginResponse{}, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(key, deriveArgon2IDKey([]byte(request.Password), salt)) {
|
||||
return AccountLoginResponse{}, fmt.Errorf("password doesn't match")
|
||||
}
|
||||
|
||||
token := make([]byte, TokenSize)
|
||||
_, err = rand.Read(token)
|
||||
if err != nil {
|
||||
return AccountLoginResponse{}, fmt.Errorf("failed to generate token: %s", err)
|
||||
}
|
||||
|
||||
err = db.AddAccountSession(request.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) != 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
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
type GenericAuthRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type GenericAuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
const (
|
||||
ArgonTime = 1
|
||||
ArgonMemory = 256 * 1024
|
||||
ArgonThreads = 4
|
||||
ArgonKeySize = 32
|
||||
ArgonSaltSize = 16
|
||||
)
|
||||
|
||||
var isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString
|
||||
|
||||
func deriveArgon2IDKey(password, salt []byte) []byte {
|
||||
return argon2.IDKey(password, salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/pagefaultgames/pokerogue-server/db"
|
||||
)
|
||||
|
||||
type LoginRequest GenericAuthRequest
|
||||
type LoginResponse GenericAuthResponse
|
||||
|
||||
// /account/login - log into account
|
||||
func Login(request LoginRequest) (LoginResponse, error) {
|
||||
if !isValidUsername(request.Username) {
|
||||
return LoginResponse{}, fmt.Errorf("invalid username")
|
||||
}
|
||||
|
||||
if len(request.Password) < 6 {
|
||||
return LoginResponse{}, fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
key, salt, err := db.FetchAccountKeySaltFromUsername(request.Username)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return LoginResponse{}, fmt.Errorf("account doesn't exist")
|
||||
}
|
||||
|
||||
return LoginResponse{}, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(key, deriveArgon2IDKey([]byte(request.Password), salt)) {
|
||||
return LoginResponse{}, fmt.Errorf("password doesn't match")
|
||||
}
|
||||
|
||||
token := make([]byte, TokenSize)
|
||||
_, err = rand.Read(token)
|
||||
if err != nil {
|
||||
return LoginResponse{}, fmt.Errorf("failed to generate token: %s", err)
|
||||
}
|
||||
|
||||
err = db.AddAccountSession(request.Username, token)
|
||||
if err != nil {
|
||||
return LoginResponse{}, fmt.Errorf("failed to add account session")
|
||||
}
|
||||
|
||||
return LoginResponse{Token: base64.StdEncoding.EncodeToString(token)}, nil
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"github.com/pagefaultgames/pokerogue-server/db"
|
||||
)
|
||||
|
||||
const (
|
||||
UUIDSize = 16
|
||||
TokenSize = 32
|
||||
)
|
||||
|
||||
type RegisterRequest GenericAuthRequest
|
||||
|
||||
// /account/register - register account
|
||||
func Register(request RegisterRequest) error {
|
||||
if !isValidUsername(request.Username) {
|
||||
return fmt.Errorf("invalid username")
|
||||
}
|
||||
|
||||
if len(request.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, request.Username, deriveArgon2IDKey([]byte(request.Password), salt), salt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add account record: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package api
|
||||
|
||||
import "golang.org/x/crypto/argon2"
|
||||
|
||||
const (
|
||||
ArgonTime = 1
|
||||
ArgonMemory = 256 * 1024
|
||||
ArgonThreads = 4
|
||||
ArgonKeySize = 32
|
||||
ArgonSaltSize = 16
|
||||
)
|
||||
|
||||
func deriveArgon2IDKey(password, salt []byte) []byte {
|
||||
return argon2.IDKey(password, salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize)
|
||||
}
|
|
@ -5,9 +5,15 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pagefaultgames/pokerogue-server/api/daily"
|
||||
"github.com/pagefaultgames/pokerogue-server/db"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
scheduleStatRefresh()
|
||||
daily.Init()
|
||||
}
|
||||
|
||||
func getUsernameFromRequest(r *http.Request) (string, error) {
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
return "", fmt.Errorf("missing token")
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package daily
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/pagefaultgames/pokerogue-server/db"
|
||||
"github.com/pagefaultgames/pokerogue-server/defs"
|
||||
)
|
||||
|
||||
const secondsPerDay = 60 * 60 * 24
|
||||
|
@ -20,22 +19,9 @@ 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 {
|
||||
func Init() error {
|
||||
secret, err := os.ReadFile("secret.key")
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
|
@ -58,18 +44,34 @@ func InitDailyRun() error {
|
|||
|
||||
dailyRunSecret = secret
|
||||
|
||||
dailyRunSeed = base64.StdEncoding.EncodeToString(deriveDailyRunSeed(time.Now().UTC()))
|
||||
|
||||
err = db.TryAddDailyRun(dailyRunSeed)
|
||||
err = db.TryAddDailyRun(Seed())
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
log.Printf("Daily Run Seed: %s", dailyRunSeed)
|
||||
log.Printf("Daily Run Seed: %s", Seed())
|
||||
|
||||
scheduleRefresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Seed() string {
|
||||
return base64.StdEncoding.EncodeToString(deriveDailyRunSeed(time.Now().UTC()))
|
||||
}
|
||||
|
||||
func scheduleRefresh() {
|
||||
dailyRunScheduler.Every(1).Day().At("00:00").Do(func() error {
|
||||
err := Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}())
|
||||
dailyRunScheduler.StartAsync()
|
||||
}
|
||||
|
||||
func deriveDailyRunSeed(seedTime time.Time) []byte {
|
||||
day := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(day, uint64(seedTime.Unix()/secondsPerDay))
|
||||
|
@ -78,28 +80,3 @@ func deriveDailyRunSeed(seedTime time.Time) []byte {
|
|||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
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
|
||||
}
|
|
@ -9,6 +9,9 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
@ -52,7 +55,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
response, err := handleAccountInfo(username, uuid)
|
||||
response, err := account.Info(username, uuid)
|
||||
if err != nil {
|
||||
httpError(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -64,14 +67,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
case "/account/register":
|
||||
var request AccountRegisterRequest
|
||||
var request account.RegisterRequest
|
||||
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)
|
||||
err = account.Register(request)
|
||||
if err != nil {
|
||||
httpError(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -79,14 +82,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case "/account/login":
|
||||
var request AccountLoginRequest
|
||||
var request account.LoginRequest
|
||||
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
|
||||
}
|
||||
|
||||
response, err := handleAccountLogin(request)
|
||||
response, err := account.Login(request)
|
||||
if err != nil {
|
||||
httpError(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -104,7 +107,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
err = handleAccountLogout(token)
|
||||
err = account.Logout(token)
|
||||
if err != nil {
|
||||
httpError(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -180,14 +183,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
switch r.URL.Path {
|
||||
case "/savedata/get":
|
||||
save, err = handleSavedataGet(uuid, datatype, slot)
|
||||
save, err = savedata.Get(uuid, datatype, slot)
|
||||
case "/savedata/update":
|
||||
err = handleSavedataUpdate(uuid, slot, save)
|
||||
err = savedata.Update(uuid, slot, save)
|
||||
case "/savedata/delete":
|
||||
err = handleSavedataDelete(uuid, datatype, slot)
|
||||
err = savedata.Delete(uuid, datatype, slot)
|
||||
case "/savedata/clear":
|
||||
// doesn't return a save, but it works
|
||||
save, err = handleSavedataClear(uuid, slot, save.(defs.SessionSaveData))
|
||||
save, err = savedata.Clear(uuid, slot, daily.Seed(), save.(defs.SessionSaveData))
|
||||
}
|
||||
if err != nil {
|
||||
httpError(w, r, err, http.StatusInternalServerError)
|
||||
|
@ -207,7 +210,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// /daily
|
||||
case "/daily/seed":
|
||||
w.Write([]byte(dailyRunSeed))
|
||||
w.Write([]byte(daily.Seed()))
|
||||
case "/daily/rankings":
|
||||
uuid, err := getUUIDFromRequest(r)
|
||||
if err != nil {
|
||||
|
@ -233,7 +236,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
rankings, err := handleRankings(uuid, category, page)
|
||||
rankings, err := daily.Rankings(uuid, category, page)
|
||||
if err != nil {
|
||||
httpError(w, r, err, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -255,7 +258,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
count, err := handleRankingPageCount(category)
|
||||
count, err := daily.RankingPageCount(category)
|
||||
if err != nil {
|
||||
httpError(w, r, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
@ -268,14 +271,3 @@ 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)
|
||||
}
|
||||
|
||||
// auth
|
||||
|
||||
type GenericAuthRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type GenericAuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
222
api/savedata.go
222
api/savedata.go
|
@ -1,222 +0,0 @@
|
|||
package api
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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, 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 >= 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
|
||||
}
|
||||
|
||||
// /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) {
|
||||
var response SavedataClearResponse
|
||||
err := db.UpdateAccountLastActivity(uuid)
|
||||
if err != nil {
|
||||
log.Print("failed to update account last activity")
|
||||
}
|
||||
|
||||
if slot < 0 || slot >= sessionSlotCount {
|
||||
return response, fmt.Errorf("slot id %d out of range", slot)
|
||||
}
|
||||
|
||||
sessionCompleted := validateSessionCompleted(save)
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package savedata
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
|
@ -0,0 +1,48 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
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 k, v := range compensations {
|
||||
typeKey := strconv.Itoa(k)
|
||||
system.VoucherCounts[typeKey] += v
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -15,7 +15,7 @@ var (
|
|||
classicSessionCount int
|
||||
)
|
||||
|
||||
func ScheduleStatRefresh() {
|
||||
func scheduleStatRefresh() {
|
||||
statScheduler.Every(10).Second().Do(updateStats)
|
||||
statScheduler.StartAsync()
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package defs
|
||||
|
||||
const SessionSlotCount = 3
|
||||
|
||||
type SystemSaveData struct {
|
||||
TrainerId int `json:"trainerId"`
|
||||
SecretId int `json:"secretId"`
|
||||
|
|
|
@ -43,9 +43,7 @@ func main() {
|
|||
os.Chmod(*addr, 0777)
|
||||
}
|
||||
|
||||
api.ScheduleStatRefresh()
|
||||
api.ScheduleDailyRunRefresh()
|
||||
api.InitDailyRun()
|
||||
api.Init()
|
||||
|
||||
err = http.Serve(listener, &api.Server{Debug: *debug})
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in New Issue