mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-05-09 04:23:26 -05:00
Use flexible database for SAKE storage
This commit is contained in:
parent
713826f30e
commit
94c718f6d2
|
|
@ -1,4 +1,16 @@
|
|||
package common
|
||||
|
||||
import "reflect"
|
||||
|
||||
func UNUSED(v ...interface{}) {
|
||||
}
|
||||
|
||||
func ReverseMap(m interface{}) interface{} {
|
||||
inputType := reflect.TypeOf(m)
|
||||
inputValue := reflect.ValueOf(m)
|
||||
result := reflect.MakeMap(reflect.MapOf(inputType.Elem(), inputType.Key()))
|
||||
for _, key := range inputValue.MapKeys() {
|
||||
result.SetMapIndex(inputValue.MapIndex(key), key)
|
||||
}
|
||||
return result.Interface()
|
||||
}
|
||||
|
|
|
|||
154
database/sake.go
Normal file
154
database/sake.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgconn"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
)
|
||||
|
||||
const (
|
||||
SakeFieldTypeByte = 0
|
||||
SakeFieldTypeShort = 1
|
||||
SakeFieldTypeInt = 2
|
||||
SakeFieldTypeFloat = 3
|
||||
SakeFieldTypeAsciiString = 4
|
||||
SakeFieldTypeUnicodeString = 5
|
||||
SakeFieldTypeBoolean = 6
|
||||
SakeFieldTypeDateAndTime = 7
|
||||
SakeFieldTypeBinaryData = 8
|
||||
SakeFieldTypeInt64 = 9
|
||||
)
|
||||
|
||||
type SakeFieldType int
|
||||
|
||||
type SakeField struct {
|
||||
Type SakeFieldType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type SakeRecord struct {
|
||||
GameId int
|
||||
OwnerId int32
|
||||
TableId string
|
||||
RecordId int32
|
||||
Fields map[string]SakeField
|
||||
}
|
||||
|
||||
const (
|
||||
getSakeRecordsQuery = `
|
||||
SELECT owner_id, record_id, fields
|
||||
FROM sake_records
|
||||
WHERE game_id = $1
|
||||
AND table_id = $2
|
||||
AND (cardinality($4::integer[]) = 0 OR record_id = ANY($4::integer[]))
|
||||
AND (cardinality($3::integer[]) = 0 OR owner_id = ANY($3::integer[]))`
|
||||
updateSakeRecordQuery = `
|
||||
UPDATE sake_records
|
||||
SET
|
||||
fields = CASE WHEN owner_id = $4 THEN fields || $5 ELSE fields END,
|
||||
update_time = CASE WHEN owner_id = $4 THEN CURRENT_TIMESTAMP ELSE update_time END
|
||||
WHERE game_id = $1
|
||||
AND table_id = $2
|
||||
AND record_id = $3
|
||||
RETURNING owner_id`
|
||||
insertSakeRecordQuery = `
|
||||
INSERT INTO sake_records (game_id, table_id, owner_id, fields)
|
||||
VALUES ($1, $2, $3, $4)`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSakeNotOwned = errors.New("record is not owned by the specified owner ID")
|
||||
)
|
||||
|
||||
func parseSakeFieldsFromJson(fieldsJson []byte) (map[string]SakeField, error) {
|
||||
var fields map[string]SakeField
|
||||
err := json.Unmarshal(fieldsJson, &fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func GetSakeRecords(pool *pgxpool.Pool, ctx context.Context, gameId int, ownerIds []int32, tableId string, recordIds []int32, fields []string, filter string) ([]SakeRecord, error) {
|
||||
if fields == nil {
|
||||
fields = []string{}
|
||||
}
|
||||
if ownerIds == nil {
|
||||
ownerIds = []int32{}
|
||||
}
|
||||
if recordIds == nil {
|
||||
recordIds = []int32{}
|
||||
}
|
||||
|
||||
rows, err := pool.Query(ctx, getSakeRecordsQuery, gameId, tableId, ownerIds, recordIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []SakeRecord
|
||||
for rows.Next() {
|
||||
record := SakeRecord{
|
||||
GameId: gameId,
|
||||
TableId: tableId,
|
||||
}
|
||||
var fieldsJson []byte
|
||||
if err := rows.Scan(&record.OwnerId, &record.RecordId, &fieldsJson); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields, err := parseSakeFieldsFromJson(fieldsJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record.Fields = fields
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func UpdateSakeRecord(pool *pgxpool.Pool, ctx context.Context, record SakeRecord, ownerId int32) error {
|
||||
fieldsJson, err := json.Marshal(record.Fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var existingOwnerId int32
|
||||
err = pool.QueryRow(ctx, updateSakeRecordQuery, record.GameId, record.TableId, record.RecordId, ownerId, fieldsJson).Scan(&existingOwnerId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ownerId != 0 && existingOwnerId != ownerId {
|
||||
return ErrSakeNotOwned
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertSakeRecord(pool *pgxpool.Pool, ctx context.Context, record SakeRecord) error {
|
||||
fieldsJson, err := json.Marshal(record.Fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err = pool.Exec(ctx, insertSakeRecordQuery, record.GameId, record.TableId, record.OwnerId, fieldsJson)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
var pgErr *pgconn.PgError
|
||||
if !errors.As(err, &pgErr) {
|
||||
break
|
||||
}
|
||||
if pgErr.Code != pgerrcode.UniqueViolation {
|
||||
break
|
||||
}
|
||||
// Retry if unique violation occurred, as the record ID is generated randomly
|
||||
}
|
||||
return nil
|
||||
}
|
||||
8
go.mod
8
go.mod
|
|
@ -8,17 +8,21 @@ require (
|
|||
github.com/jackc/pgx/v4 v4.18.3
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0
|
||||
golang.org/x/net v0.47.0
|
||||
github.com/linkdata/deadlock v0.5.5 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250512220230-2268d0cbb0f5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.3 // indirect
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgtype v1.14.4 // indirect
|
||||
github.com/jackc/puddle v1.3.0 // indirect
|
||||
github.com/linkdata/deadlock v0.5.5 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250512220230-2268d0cbb0f5
|
||||
)
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -26,6 +26,8 @@ github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8
|
|||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
|
|
@ -78,6 +80,8 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
|||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/linkdata/deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU=
|
||||
github.com/linkdata/deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U=
|
||||
github.com/linkdata/deadlock v0.5.5 h1:d6O+rzEqasSfamGDA8u7bjtaq7hOX8Ha4Zn36Wxrkvo=
|
||||
github.com/linkdata/deadlock v0.5.5/go.mod h1:tXb28stzAD3trzEEK0UJWC+rZKuobCoPktPYzebb1u0=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4=
|
||||
|
|
@ -101,8 +105,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
|||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/linkdata/deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU=
|
||||
github.com/linkdata/deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"wwfc/common"
|
||||
|
|
@ -29,6 +30,150 @@ const (
|
|||
rkgdFileName = "ghost.bin"
|
||||
)
|
||||
|
||||
var (
|
||||
ghostDataFilterRegex = regexp.MustCompile(`^course = ([1-9]\d?|0) and gameid = 1687 and time < ([1-9][0-9]{0,5})$`)
|
||||
)
|
||||
|
||||
func getMarioKartWiiGhostDataRecord(moduleName string, request StorageRequestData) (database.SakeRecord, bool) {
|
||||
if request.Sort != "time desc" {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid sort string:", aurora.Cyan(request.Sort))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Offset != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid offset value:", aurora.Cyan(request.Offset))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Max != 1 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid number of records to return:", aurora.Cyan(request.Max))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Surrounding != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid number of surrounding records to return:", aurora.Cyan(request.Surrounding))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if len(request.OwnerIDs.OwnerID) != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid owner id array:", aurora.Cyan(request.OwnerIDs))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.CacheFlag != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid cache value:", aurora.Cyan(request.CacheFlag))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
match := ghostDataFilterRegex.FindStringSubmatch(request.Filter)
|
||||
if match == nil {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid filter string:", aurora.Cyan(request.Filter))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
courseIdInt, _ := strconv.Atoi(match[1])
|
||||
courseId := common.MarioKartWiiCourseId(courseIdInt)
|
||||
if !courseId.IsValid() {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid course ID:", aurora.Cyan(match[1]))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
time, _ := strconv.Atoi(match[2])
|
||||
if time >= 360000 /* 6 minutes */ {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Invalid time:", aurora.Cyan(match[2]))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
fileId, err := database.GetMarioKartWiiGhostData(pool, ctx, courseId, time)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "mariokartwii/GhostData: Failed to get the ghost data from the database:", err)
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
return database.SakeRecord{
|
||||
GameId: 1687,
|
||||
TableId: "GhostData",
|
||||
RecordId: 0,
|
||||
OwnerId: 0,
|
||||
Fields: map[string]database.SakeField{
|
||||
"fileid": {
|
||||
Type: database.SakeFieldTypeInt,
|
||||
Value: strconv.FormatInt(int64(int32(fileId)), 10),
|
||||
},
|
||||
},
|
||||
}, true
|
||||
}
|
||||
|
||||
func getMarioKartWiiStoredGhostDataRecord(moduleName string, request StorageRequestData) (database.SakeRecord, bool) {
|
||||
if request.Sort != "time" {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid sort string:", aurora.Cyan(request.Sort))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Offset != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid offset value:", aurora.Cyan(request.Offset))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Max != 1 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid number of records to return:", aurora.Cyan(request.Max))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.Surrounding != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid number of surrounding records to return:", aurora.Cyan(request.Surrounding))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if len(request.OwnerIDs.OwnerID) != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid owner id array:", aurora.Cyan(request.OwnerIDs))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
if request.CacheFlag != 0 {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid cache value:", aurora.Cyan(request.CacheFlag))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`^course = ([1-9]\d?|0) and gameid = 1687(?: and region = ([1-7]))?$`).FindStringSubmatch(request.Filter)
|
||||
if match == nil {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid filter string:", aurora.Cyan(request.Filter))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
courseIdInt, _ := strconv.Atoi(match[1])
|
||||
courseId := common.MarioKartWiiCourseId(courseIdInt)
|
||||
if !courseId.IsValid() {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Invalid course ID:", aurora.Cyan(match[1]))
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
var regionId common.MarioKartWiiLeaderboardRegionId
|
||||
if regionIdExists := match[2] != ""; regionIdExists {
|
||||
regionIdInt, _ := strconv.Atoi(match[2])
|
||||
regionId = common.MarioKartWiiLeaderboardRegionId(regionIdInt)
|
||||
} else {
|
||||
regionId = common.Worldwide
|
||||
}
|
||||
|
||||
pid, fileId, err := database.GetMarioKartWiiStoredGhostData(pool, ctx, regionId, courseId)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "mariokartwii/StoredGhostData: Failed to get the stored ghost data from the database:", err)
|
||||
return database.SakeRecord{}, false
|
||||
}
|
||||
|
||||
return database.SakeRecord{
|
||||
GameId: 1687,
|
||||
TableId: "StoredGhostData",
|
||||
RecordId: 0,
|
||||
OwnerId: int32(pid),
|
||||
Fields: map[string]database.SakeField{
|
||||
"profile": {Type: database.SakeFieldTypeInt, Value: strconv.FormatInt(int64(int32(pid)), 10)},
|
||||
"fileid": {Type: database.SakeFieldTypeInt, Value: strconv.FormatInt(int64(int32(fileId)), 10)},
|
||||
},
|
||||
}, true
|
||||
}
|
||||
|
||||
func handleMarioKartWiiFileDownloadRequest(moduleName string, responseWriter http.ResponseWriter, request *http.Request) {
|
||||
if strings.HasSuffix(request.URL.Path, "ghostdownload.aspx") {
|
||||
handleMarioKartWiiGhostDownloadRequest(moduleName, responseWriter, request)
|
||||
|
|
|
|||
623
sake/storage.go
623
sake/storage.go
|
|
@ -1,20 +1,43 @@
|
|||
package sake
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
"wwfc/common"
|
||||
"wwfc/database"
|
||||
"wwfc/logging"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
ResultSuccess = "Success"
|
||||
ResultSecretKeyInvalid = "SecretKeyInvalid"
|
||||
ResultServiceDisabled = "ServiceDisabled"
|
||||
ResultDatabaseUnavailable = "DatabaseUnavailable"
|
||||
ResultLoginTicketInvalid = "LoginTicketInvalid"
|
||||
ResultLoginTicketExpired = "LoginTicketExpired"
|
||||
ResultTableNotFound = "TableNotFound"
|
||||
ResultRecordNotFound = "RecordNotFound"
|
||||
ResultFieldNotFound = "FieldNotFound"
|
||||
ResultFieldTypeInvalid = "FieldTypeInvalid"
|
||||
ResultNoPermission = "NoPermission"
|
||||
ResultRecordLimitReached = "RecordLimitReached"
|
||||
ResultAlreadyRated = "AlreadyRated"
|
||||
ResultNotRateable = "NotRateable"
|
||||
ResultNotOwned = "NotOwned"
|
||||
ResultFilterInvalid = "FilterInvalid"
|
||||
ResultSortInvalid = "SortInvalid"
|
||||
ResultTargetFilterInvalid = "TargetFilterInvalid"
|
||||
ResultUnknownError = "UnknownError"
|
||||
)
|
||||
|
||||
const (
|
||||
SOAPEnvNamespace = "http://schemas.xmlsoap.org/soap/envelope/"
|
||||
SakeNamespace = "http://gamespy.net/sake"
|
||||
|
|
@ -36,13 +59,13 @@ type StorageRequestData struct {
|
|||
SecretKey string `xml:"secretKey"`
|
||||
LoginTicket string `xml:"loginTicket"`
|
||||
TableID string `xml:"tableid"`
|
||||
RecordID string `xml:"recordid"`
|
||||
RecordID int32 `xml:"recordid"`
|
||||
Filter string `xml:"filter"`
|
||||
Sort string `xml:"sort"`
|
||||
Offset int `xml:"offset"`
|
||||
Max int `xml:"max"`
|
||||
Surrounding int `xml:"surrounding"`
|
||||
OwnerIDs string `xml:"ownerids"`
|
||||
OwnerIDs StorageOwnerIDs `xml:"ownerids"`
|
||||
CacheFlag int `xml:"cacheFlag"`
|
||||
Fields StorageFields `xml:"fields"`
|
||||
Values StorageUpdateRecordValues `xml:"values"`
|
||||
|
|
@ -72,46 +95,68 @@ type StorageValue struct {
|
|||
Value string `xml:"value"`
|
||||
}
|
||||
|
||||
type StorageOwnerIDs struct {
|
||||
OwnerID []int32 `xml:"int"`
|
||||
}
|
||||
|
||||
type StorageResponseEnvelope struct {
|
||||
XMLName xml.Name
|
||||
Body StorageResponseBody
|
||||
Body StorageResponseBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
|
||||
}
|
||||
|
||||
type StorageResponseBody struct {
|
||||
XMLName xml.Name
|
||||
GetMyRecordsResponse *StorageGetMyRecordsResponse `xml:"http://gamespy.net/sake GetMyRecordsResponse"`
|
||||
UpdateRecordResponse *StorageUpdateRecordResponse `xml:"http://gamespy.net/sake UpdateRecordResponse"`
|
||||
SearchForRecordsResponse *StorageSearchForRecordsResponse `xml:"http://gamespy.net/sake SearchForRecordsResponse"`
|
||||
}
|
||||
|
||||
type StorageGetMyRecordsResponse struct {
|
||||
XMLName xml.Name
|
||||
GetMyRecordsResult string
|
||||
Values StorageResponseValues `xml:"values"` // ???
|
||||
Values StorageResponseValues `xml:"values"`
|
||||
}
|
||||
|
||||
type StorageResponseValues struct {
|
||||
XMLName xml.Name
|
||||
ArrayOfRecordValue StorageArrayOfRecordValue
|
||||
ArrayOfRecordValue []StorageArrayOfRecordValue `xml:"ArrayOfRecordValue"`
|
||||
}
|
||||
|
||||
type StorageArrayOfRecordValue struct {
|
||||
XMLName xml.Name
|
||||
RecordValues []StorageRecordValue `xml:"RecordValue"`
|
||||
}
|
||||
|
||||
type StorageUpdateRecordResponse struct {
|
||||
XMLName xml.Name
|
||||
UpdateRecordResult string
|
||||
// TODO
|
||||
}
|
||||
|
||||
type StorageSearchForRecordsResponse struct {
|
||||
XMLName xml.Name
|
||||
SearchForRecordsResult string
|
||||
Values StorageResponseValues `xml:"values"` // ???
|
||||
Values StorageResponseValues `xml:"values"`
|
||||
}
|
||||
|
||||
var (
|
||||
sakeTypeToTag = map[database.SakeFieldType]string{
|
||||
database.SakeFieldTypeByte: "byteValue",
|
||||
database.SakeFieldTypeShort: "shortValue",
|
||||
database.SakeFieldTypeInt: "intValue",
|
||||
database.SakeFieldTypeFloat: "floatValue",
|
||||
database.SakeFieldTypeAsciiString: "asciiStringValue",
|
||||
database.SakeFieldTypeUnicodeString: "unicodeStringValue",
|
||||
database.SakeFieldTypeBoolean: "booleanValue",
|
||||
database.SakeFieldTypeDateAndTime: "dateAndTimeValue",
|
||||
database.SakeFieldTypeBinaryData: "binaryDataValue",
|
||||
database.SakeFieldTypeInt64: "int64Value",
|
||||
}
|
||||
|
||||
tagToSakeType = common.ReverseMap(sakeTypeToTag).(map[string]database.SakeFieldType)
|
||||
|
||||
storageRequestHandlers = map[string]func(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) StorageResponseBody{
|
||||
SakeNamespace + "/CreateRecord": createRecord,
|
||||
SakeNamespace + "/UpdateRecord": updateRecord,
|
||||
SakeNamespace + "/GetMyRecords": getMyRecords,
|
||||
SakeNamespace + "/SearchForRecords": searchForRecords,
|
||||
}
|
||||
)
|
||||
|
||||
func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Request) {
|
||||
headerAction := r.Header.Get("SOAPAction")
|
||||
if headerAction == "" {
|
||||
|
|
@ -124,6 +169,8 @@ func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Requ
|
|||
panic(err)
|
||||
}
|
||||
|
||||
logging.Info("SAKE", "Received storage request with SOAPAction:", aurora.Yellow(headerAction), "and body:", aurora.Cyan(string(body)))
|
||||
|
||||
// Parse the SOAP request XML
|
||||
soap := StorageRequestEnvelope{}
|
||||
err = xml.Unmarshal(body, &soap)
|
||||
|
|
@ -134,29 +181,23 @@ func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Requ
|
|||
|
||||
response := StorageResponseEnvelope{
|
||||
XMLName: xml.Name{Space: SOAPEnvNamespace, Local: "Envelope"},
|
||||
Body: StorageResponseBody{
|
||||
XMLName: xml.Name{Space: SOAPEnvNamespace, Local: "Body"},
|
||||
},
|
||||
}
|
||||
|
||||
xmlName := soap.Body.Data.XMLName.Space + "/" + soap.Body.Data.XMLName.Local
|
||||
if headerAction == xmlName || headerAction == `"`+xmlName+`"` {
|
||||
logging.Info(moduleName, "SOAPAction:", aurora.Yellow(soap.Body.Data.XMLName.Local))
|
||||
|
||||
if profileId, gameInfo, ok := getRequestIdentity(moduleName, soap.Body.Data); ok {
|
||||
switch xmlName {
|
||||
case SakeNamespace + "/GetMyRecords":
|
||||
response.Body.GetMyRecordsResponse = getMyRecords(moduleName, profileId, gameInfo, soap.Body.Data)
|
||||
handler, ok := storageRequestHandlers[xmlName]
|
||||
if !ok {
|
||||
panic("unknown SOAPAction: " + aurora.Cyan(xmlName).String())
|
||||
}
|
||||
|
||||
case SakeNamespace + "/UpdateRecord":
|
||||
response.Body.UpdateRecordResponse = updateRecord(moduleName, profileId, gameInfo, soap.Body.Data)
|
||||
|
||||
case SakeNamespace + "/SearchForRecords":
|
||||
response.Body.SearchForRecordsResponse = searchForRecords(moduleName, gameInfo, soap.Body.Data)
|
||||
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown SOAPAction:", aurora.Cyan(xmlName))
|
||||
}
|
||||
profileId, gameInfo, errorString := getRequestIdentity(moduleName, soap.Body.Data)
|
||||
if errorString != ResultSuccess {
|
||||
logging.Error(moduleName, "Failed to get request identity:", aurora.Cyan(errorString))
|
||||
response.Body.setResultTag(xmlName, errorString)
|
||||
} else {
|
||||
response.Body = handler(moduleName, profileId, gameInfo, soap.Body.Data)
|
||||
}
|
||||
} else {
|
||||
logging.Error(moduleName, "Invalid SOAPAction or XML request:", aurora.Cyan(headerAction))
|
||||
|
|
@ -174,316 +215,243 @@ func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Requ
|
|||
w.Write(payload)
|
||||
}
|
||||
|
||||
func getRequestIdentity(moduleName string, request StorageRequestData) (uint32, common.GameInfo, bool) {
|
||||
func getRequestIdentity(moduleName string, request StorageRequestData) (uint32, common.GameInfo, string) {
|
||||
gameInfo := common.GetGameInfoByID(request.GameID)
|
||||
if gameInfo == nil {
|
||||
logging.Error(moduleName, "Invalid game ID:", aurora.Cyan(request.GameID))
|
||||
return 0, common.GameInfo{}, false
|
||||
return 0, common.GameInfo{}, ResultDatabaseUnavailable
|
||||
}
|
||||
|
||||
if gameInfo.SecretKey != request.SecretKey {
|
||||
logging.Error(moduleName, "Mismatch", aurora.BrightCyan(gameInfo.Name), "secret key:", aurora.Cyan(request.SecretKey), "!=", aurora.BrightCyan(gameInfo.SecretKey))
|
||||
return 0, common.GameInfo{}, false
|
||||
return 0, common.GameInfo{}, ResultSecretKeyInvalid
|
||||
}
|
||||
|
||||
profileId, _, err := common.UnmarshalGPCMLoginTicket(request.LoginTicket)
|
||||
profileId, issueTime, err := common.UnmarshalGPCMLoginTicket(request.LoginTicket)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, err)
|
||||
return 0, common.GameInfo{}, false
|
||||
return 0, common.GameInfo{}, ResultLoginTicketInvalid
|
||||
}
|
||||
|
||||
logging.Info(moduleName, "Profile ID:", aurora.BrightCyan(profileId))
|
||||
logging.Info(moduleName, "Game:", aurora.Cyan(request.GameID), "-", aurora.BrightCyan(gameInfo.Name))
|
||||
logging.Info(moduleName, "Table ID:", aurora.Cyan(request.TableID))
|
||||
if issueTime.Add(48 * time.Hour).Before(time.Now()) {
|
||||
return 0, common.GameInfo{}, ResultLoginTicketExpired
|
||||
}
|
||||
|
||||
return profileId, *gameInfo, true
|
||||
return profileId, *gameInfo, ResultSuccess
|
||||
}
|
||||
|
||||
func binaryDataValue(value []byte) StorageValue {
|
||||
return StorageValue{
|
||||
XMLName: xml.Name{Local: "binaryDataValue"},
|
||||
Value: base64.StdEncoding.EncodeToString(value),
|
||||
func getInputFields(moduleName string, request StorageRequestData) (map[string]database.SakeField, string) {
|
||||
record := database.SakeRecord{
|
||||
Fields: make(map[string]database.SakeField),
|
||||
}
|
||||
}
|
||||
|
||||
func binaryDataValueBase64(value string) StorageValue {
|
||||
return StorageValue{
|
||||
XMLName: xml.Name{Local: "binaryDataValue"},
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func intValue(value int32) StorageValue {
|
||||
return StorageValue{
|
||||
XMLName: xml.Name{Local: "intValue"},
|
||||
Value: strconv.FormatInt(int64(value), 10),
|
||||
}
|
||||
}
|
||||
|
||||
// I don't even know if this is a thing
|
||||
func uintValue(value uint32) StorageValue {
|
||||
return StorageValue{
|
||||
XMLName: xml.Name{Local: "uintValue"},
|
||||
Value: strconv.FormatUint(uint64(value), 10),
|
||||
}
|
||||
}
|
||||
|
||||
func getMyRecords(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) *StorageGetMyRecordsResponse {
|
||||
errorResponse := StorageGetMyRecordsResponse{
|
||||
GetMyRecordsResult: "Error",
|
||||
}
|
||||
|
||||
var values map[string]StorageValue
|
||||
|
||||
switch gameInfo.Name + "/" + request.TableID {
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown table")
|
||||
for _, field := range request.Fields.Fields {
|
||||
logging.Info(moduleName, "Field:", aurora.Cyan(field))
|
||||
for _, field := range request.Values.RecordFields {
|
||||
fieldType, ok := tagToSakeType[field.Value.Value.XMLName.Local]
|
||||
if !ok {
|
||||
logging.Error(moduleName, "Invalid field type tag:", aurora.Cyan(field.Value.Value.XMLName.Local))
|
||||
return nil, ResultFieldTypeInvalid
|
||||
}
|
||||
return &errorResponse
|
||||
|
||||
case "mariokartwii/FriendInfo":
|
||||
// Mario Kart Wii friend info
|
||||
values = map[string]StorageValue{
|
||||
"ownerid": uintValue(profileId),
|
||||
"recordid": intValue(int32(profileId)),
|
||||
"info": binaryDataValueBase64(database.GetMKWFriendInfo(pool, ctx, profileId)),
|
||||
if field.Name == "ownerid" || field.Name == "recordid" {
|
||||
logging.Error(moduleName, "Attempt to set reserved field:", aurora.Cyan(field.Name))
|
||||
return nil, ResultNoPermission
|
||||
}
|
||||
record.Fields[field.Name] = database.SakeField{Type: fieldType, Value: field.Value.Value.Value}
|
||||
}
|
||||
return record.Fields, ResultSuccess
|
||||
}
|
||||
|
||||
func createRecord(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) StorageResponseBody {
|
||||
if request.TableID == "" {
|
||||
logging.Error(moduleName, "No table ID provided")
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultTableNotFound,
|
||||
}}
|
||||
}
|
||||
|
||||
if gameInfo.Name == "mariokartwii" && (request.TableID == "GhostData" || request.TableID == "StoredGhostData") {
|
||||
// Reserved for special handler
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultTableNotFound,
|
||||
}}
|
||||
}
|
||||
|
||||
var record database.SakeRecord
|
||||
var result string
|
||||
record.Fields, result = getInputFields(moduleName, request)
|
||||
if result != ResultSuccess {
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: result,
|
||||
}}
|
||||
}
|
||||
|
||||
record.GameId = gameInfo.GameID
|
||||
record.TableId = request.TableID
|
||||
record.RecordId = 0
|
||||
record.OwnerId = int32(profileId)
|
||||
|
||||
// TODO: Limit number of records or fields a user can have
|
||||
err := database.InsertSakeRecord(pool, ctx, record)
|
||||
if err != nil {
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultDatabaseUnavailable,
|
||||
}}
|
||||
}
|
||||
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultSuccess,
|
||||
}}
|
||||
|
||||
}
|
||||
|
||||
func getMyRecords(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) StorageResponseBody {
|
||||
if len(request.Fields.Fields) == 0 {
|
||||
// GameSpy client doesn't consider zero fields valid
|
||||
return StorageResponseBody{GetMyRecordsResponse: &StorageGetMyRecordsResponse{
|
||||
GetMyRecordsResult: "BadNumFields",
|
||||
}}
|
||||
}
|
||||
if request.TableID == "" {
|
||||
logging.Error(moduleName, "No table ID provided")
|
||||
return StorageResponseBody{GetMyRecordsResponse: &StorageGetMyRecordsResponse{
|
||||
GetMyRecordsResult: ResultTableNotFound,
|
||||
}}
|
||||
}
|
||||
|
||||
records, err := database.GetSakeRecords(pool, ctx, gameInfo.GameID, []int32{int32(profileId)}, request.TableID, nil, request.Fields.Fields, request.Filter)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Failed to get sake records from the database:", err)
|
||||
if err == pgx.ErrNoRows {
|
||||
return StorageResponseBody{GetMyRecordsResponse: &StorageGetMyRecordsResponse{
|
||||
GetMyRecordsResult: ResultRecordNotFound,
|
||||
}}
|
||||
}
|
||||
return StorageResponseBody{GetMyRecordsResponse: &StorageGetMyRecordsResponse{
|
||||
GetMyRecordsResult: ResultDatabaseUnavailable,
|
||||
}}
|
||||
}
|
||||
|
||||
response := StorageGetMyRecordsResponse{
|
||||
GetMyRecordsResult: "Success",
|
||||
GetMyRecordsResult: ResultSuccess,
|
||||
Values: fillResponseValues(records, request),
|
||||
}
|
||||
logging.Info(moduleName, "Returning", aurora.Cyan(len(records)), "records from table", aurora.Cyan(request.TableID), "for profile", aurora.Cyan(profileId))
|
||||
|
||||
fieldCount := 0
|
||||
valueArray := &response.Values.ArrayOfRecordValue
|
||||
for _, field := range request.Fields.Fields {
|
||||
if value, ok := values[field]; ok {
|
||||
fieldCount++
|
||||
valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: &value})
|
||||
} else {
|
||||
valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: nil})
|
||||
}
|
||||
}
|
||||
|
||||
logging.Info(moduleName, "Wrote", aurora.Cyan(fieldCount), "field(s)")
|
||||
return &response
|
||||
return StorageResponseBody{GetMyRecordsResponse: &response}
|
||||
}
|
||||
|
||||
func updateRecord(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) *StorageUpdateRecordResponse {
|
||||
errorResponse := StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: "Error",
|
||||
func updateRecord(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) StorageResponseBody {
|
||||
if request.TableID == "" {
|
||||
logging.Error(moduleName, "No table ID provided")
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultTableNotFound,
|
||||
}}
|
||||
}
|
||||
|
||||
switch gameInfo.Name + "/" + request.TableID {
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown table")
|
||||
for _, field := range request.Values.RecordFields {
|
||||
logging.Info(moduleName, "Field:", aurora.Cyan(field.Name), "Type:", aurora.Cyan(field.Value.XMLName.Local), "Value:", aurora.Cyan(field.Value.Value.Value))
|
||||
if gameInfo.Name == "mariokartwii" && (request.TableID == "GhostData" || request.TableID == "StoredGhostData") {
|
||||
// Reserved for special handler
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultTableNotFound,
|
||||
}}
|
||||
}
|
||||
|
||||
var record database.SakeRecord
|
||||
var result string
|
||||
record.Fields, result = getInputFields(moduleName, request)
|
||||
if result != ResultSuccess {
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: result,
|
||||
}}
|
||||
}
|
||||
|
||||
record.GameId = gameInfo.GameID
|
||||
record.TableId = request.TableID
|
||||
record.RecordId = request.RecordID
|
||||
record.OwnerId = int32(profileId)
|
||||
|
||||
err := database.UpdateSakeRecord(pool, ctx, record, int32(profileId))
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Failed to update sake record in the database:", err)
|
||||
if err == database.ErrSakeNotOwned {
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultNotOwned,
|
||||
}}
|
||||
}
|
||||
return &errorResponse
|
||||
|
||||
case "mariokartwii/FriendInfo":
|
||||
// Mario Kart Wii friend info
|
||||
if len(request.Values.RecordFields) != 1 || request.Values.RecordFields[0].Name != "info" || request.Values.RecordFields[0].Value.Value.XMLName.Local != "binaryDataValue" {
|
||||
logging.Error(moduleName, "Invalid record fields")
|
||||
return &errorResponse
|
||||
if err == pgx.ErrNoRows {
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultRecordNotFound,
|
||||
}}
|
||||
}
|
||||
|
||||
// TODO: Validate record data
|
||||
database.UpdateMKWFriendInfo(pool, ctx, profileId, request.Values.RecordFields[0].Value.Value.Value)
|
||||
logging.Notice(moduleName, "Updated Mario Kart Wii friend info")
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultDatabaseUnavailable,
|
||||
}}
|
||||
}
|
||||
|
||||
return &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: "Success",
|
||||
}
|
||||
logging.Info(moduleName, "Updated record", aurora.Cyan(record.RecordId), "in table", aurora.Cyan(record.TableId), "for profile", aurora.Cyan(profileId))
|
||||
|
||||
return StorageResponseBody{UpdateRecordResponse: &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: ResultSuccess,
|
||||
}}
|
||||
}
|
||||
|
||||
func searchForRecords(moduleName string, gameInfo common.GameInfo, request StorageRequestData) *StorageSearchForRecordsResponse {
|
||||
errorResponse := StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: "Error",
|
||||
}
|
||||
var searchRecordHandlers = map[string]func(string, StorageRequestData) (database.SakeRecord, bool){
|
||||
"mariokartwii/GhostData": getMarioKartWiiGhostDataRecord,
|
||||
"mariokartwii/StoredGhostData": getMarioKartWiiStoredGhostDataRecord,
|
||||
}
|
||||
|
||||
var values []map[string]StorageValue
|
||||
var (
|
||||
standardFilterRegex = regexp.MustCompile(`^ownerid\s*=\s*-?(\d{1,10})$`)
|
||||
)
|
||||
|
||||
switch gameInfo.Name + "/" + request.TableID {
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown table")
|
||||
for _, field := range request.Fields.Fields {
|
||||
logging.Info(moduleName, "Field:", aurora.Cyan(field))
|
||||
// TODO: Test this
|
||||
func searchForRecords(moduleName string, profileId uint32, gameInfo common.GameInfo, request StorageRequestData) StorageResponseBody {
|
||||
var records []database.SakeRecord
|
||||
|
||||
if handler, ok := searchRecordHandlers[gameInfo.Name+"/"+request.TableID]; ok {
|
||||
record, ok := handler(moduleName, request)
|
||||
if !ok {
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: ResultUnknownError,
|
||||
}}
|
||||
}
|
||||
return &errorResponse
|
||||
records = append(records, record)
|
||||
} else {
|
||||
filter := request.Filter
|
||||
ownerIds := request.OwnerIDs.OwnerID
|
||||
|
||||
case "mariokartwii/FriendInfo":
|
||||
// Mario Kart Wii friend info
|
||||
match := regexp.MustCompile(`^ownerid = (\d{1,10})$`).FindStringSubmatch(request.Filter)
|
||||
if len(match) != 2 {
|
||||
logging.Error(moduleName, "Invalid filter")
|
||||
return &errorResponse
|
||||
// Parse standard filter
|
||||
if matches := standardFilterRegex.FindStringSubmatch(request.Filter); matches != nil {
|
||||
ownerIdParsed, err := strconv.ParseInt(matches[1], 10, 32)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid owner ID in filter:", aurora.Cyan(request.Filter))
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: ResultFilterInvalid,
|
||||
}}
|
||||
}
|
||||
ownerIds = append(ownerIds, int32(ownerIdParsed))
|
||||
filter = ""
|
||||
}
|
||||
|
||||
ownerId, err := strconv.ParseInt(match[1], 10, 32)
|
||||
var err error
|
||||
records, err = database.GetSakeRecords(pool, ctx, gameInfo.GameID, ownerIds, request.TableID, nil, request.Fields.Fields, filter)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Invalid owner ID")
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
values = []map[string]StorageValue{
|
||||
{
|
||||
"ownerid": uintValue(uint32(ownerId)),
|
||||
"recordid": intValue(int32(ownerId)),
|
||||
"info": binaryDataValueBase64(database.GetMKWFriendInfo(pool, ctx, uint32(ownerId))),
|
||||
},
|
||||
}
|
||||
|
||||
case "mariokartwii/GhostData":
|
||||
if request.TableID != "GhostData" {
|
||||
logging.Error(moduleName, "Invalid table name:", aurora.Cyan(request.TableID))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.Sort != "time desc" {
|
||||
logging.Error(moduleName, "Invalid sort string:", aurora.Cyan(request.Sort))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.Offset != 0 {
|
||||
logging.Error(moduleName, "Invalid offset value:", aurora.Cyan(request.Offset))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.Max != 1 {
|
||||
logging.Error(moduleName, "Invalid number of records to return:", aurora.Cyan(request.Max))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.Surrounding != 0 {
|
||||
logging.Error(moduleName, "Invalid number of surrounding records to return:", aurora.Cyan(request.Surrounding))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.OwnerIDs != "" {
|
||||
logging.Error(moduleName, "Invalid owner id array:", aurora.Cyan(request.OwnerIDs))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.CacheFlag != 0 {
|
||||
logging.Error(moduleName, "Invalid cache value:", aurora.Cyan(request.CacheFlag))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`^course = ([1-9]\d?|0) and gameid = 1687 and time < ([1-9][0-9]{0,5})$`).FindStringSubmatch(request.Filter)
|
||||
if match == nil {
|
||||
logging.Error(moduleName, "Invalid filter string:", aurora.Cyan(request.Filter))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
courseIdInt, _ := strconv.Atoi(match[1])
|
||||
courseId := common.MarioKartWiiCourseId(courseIdInt)
|
||||
if !courseId.IsValid() {
|
||||
logging.Error(moduleName, "Invalid course ID:", aurora.Cyan(match[1]))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
time, _ := strconv.Atoi(match[2])
|
||||
if time >= 360000 /* 6 minutes */ {
|
||||
logging.Error(moduleName, "Invalid time:", aurora.Cyan(match[2]))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
fileId, err := database.GetMarioKartWiiGhostData(pool, ctx, courseId, time)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Failed to get the ghost data from the database:", err)
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
values = []map[string]StorageValue{
|
||||
{
|
||||
"fileid": intValue(int32(fileId)),
|
||||
},
|
||||
}
|
||||
|
||||
case "mariokartwii/StoredGhostData":
|
||||
if request.Sort != "time" {
|
||||
logging.Error(moduleName, "Invalid sort string:", aurora.Cyan(request.Sort))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.Offset != 0 {
|
||||
logging.Error(moduleName, "Invalid offset value:", aurora.Cyan(request.Offset))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.Max != 1 {
|
||||
logging.Error(moduleName, "Invalid number of records to return:", aurora.Cyan(request.Max))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.Surrounding != 0 {
|
||||
logging.Error(moduleName, "Invalid number of surrounding records to return:", aurora.Cyan(request.Surrounding))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.OwnerIDs != "" {
|
||||
logging.Error(moduleName, "Invalid owner id array:", aurora.Cyan(request.OwnerIDs))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
if request.CacheFlag != 0 {
|
||||
logging.Error(moduleName, "Invalid cache value:", aurora.Cyan(request.CacheFlag))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`^course = ([1-9]\d?|0) and gameid = 1687(?: and region = ([1-7]))?$`).FindStringSubmatch(request.Filter)
|
||||
if match == nil {
|
||||
logging.Error(moduleName, "Invalid filter string:", aurora.Cyan(request.Filter))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
courseIdInt, _ := strconv.Atoi(match[1])
|
||||
courseId := common.MarioKartWiiCourseId(courseIdInt)
|
||||
if !courseId.IsValid() {
|
||||
logging.Error(moduleName, "Invalid course ID:", aurora.Cyan(match[1]))
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
var regionId common.MarioKartWiiLeaderboardRegionId
|
||||
if regionIdExists := match[2] != ""; regionIdExists {
|
||||
regionIdInt, _ := strconv.Atoi(match[2])
|
||||
regionId = common.MarioKartWiiLeaderboardRegionId(regionIdInt)
|
||||
} else {
|
||||
regionId = common.Worldwide
|
||||
}
|
||||
|
||||
pid, fileId, err := database.GetMarioKartWiiStoredGhostData(pool, ctx, regionId, courseId)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, "Failed to get the stored ghost data from the database:", err)
|
||||
return &errorResponse
|
||||
}
|
||||
|
||||
values = []map[string]StorageValue{
|
||||
{
|
||||
"profile": intValue(int32(pid)),
|
||||
"fileid": intValue(int32(fileId)),
|
||||
},
|
||||
logging.Error(moduleName, "Failed to get sake records from the database:", err)
|
||||
return StorageResponseBody{SearchForRecordsResponse: &StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: ResultDatabaseUnavailable,
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the values now
|
||||
sort.Slice(values, func(l, r int) bool {
|
||||
lVal, lExists := values[l][request.Sort]
|
||||
rVal, rExists := values[r][request.Sort]
|
||||
// Sort the records now
|
||||
sort.Slice(records, func(l, r int) bool {
|
||||
lVal, lExists := records[l].Fields[request.Sort]
|
||||
rVal, rExists := records[r].Fields[request.Sort]
|
||||
if !lExists || !rExists {
|
||||
// Prioritises the one that exists or goes left if both false
|
||||
return rExists
|
||||
}
|
||||
|
||||
if lVal.XMLName.Local != "intValue" && lVal.XMLName.Local != "uintValue" {
|
||||
panic(aurora.Cyan(lVal.XMLName.Local).String() + " used as sort value")
|
||||
if lVal.Type != database.SakeFieldTypeInt || rVal.Type != database.SakeFieldTypeInt {
|
||||
panic(aurora.Cyan(lVal.Type).String() + " used as sort value")
|
||||
}
|
||||
// Assuming the two use the same type
|
||||
|
||||
lValInt, err := strconv.ParseInt(lVal.Value, 10, 64)
|
||||
if err != nil {
|
||||
|
|
@ -497,24 +465,79 @@ func searchForRecords(moduleName string, gameInfo common.GameInfo, request Stora
|
|||
return lValInt < rValInt
|
||||
})
|
||||
|
||||
// Enforce the maximum number of records after sorting
|
||||
if request.Max > 0 && len(records) > request.Max {
|
||||
records = records[:request.Max]
|
||||
}
|
||||
|
||||
response := StorageSearchForRecordsResponse{
|
||||
SearchForRecordsResult: "Success",
|
||||
Values: fillResponseValues(records, request),
|
||||
}
|
||||
logging.Info(moduleName, "Searched for records in table", aurora.Cyan(request.TableID), "for profile", aurora.Cyan(profileId), "with filter", aurora.Cyan(request.Filter), "and got", aurora.Cyan(len(records)), "records")
|
||||
|
||||
fieldCount := 0
|
||||
valueArray := &response.Values.ArrayOfRecordValue
|
||||
var i int
|
||||
for i = 0; i < len(values) && i < request.Max; i++ {
|
||||
return StorageResponseBody{SearchForRecordsResponse: &response}
|
||||
}
|
||||
|
||||
func fillResponseValues(records []database.SakeRecord, request StorageRequestData) StorageResponseValues {
|
||||
var response StorageResponseValues
|
||||
for _, record := range records {
|
||||
valueArray := StorageArrayOfRecordValue{}
|
||||
for _, field := range request.Fields.Fields {
|
||||
if value, ok := values[i][field]; ok {
|
||||
fieldCount++
|
||||
valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: &value})
|
||||
} else {
|
||||
if field == "ownerid" {
|
||||
valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: &StorageValue{
|
||||
XMLName: xml.Name{Local: "intValue"},
|
||||
Value: strconv.FormatInt(int64(int32(record.OwnerId)), 10),
|
||||
}})
|
||||
continue
|
||||
}
|
||||
if field == "recordid" {
|
||||
valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: &StorageValue{
|
||||
XMLName: xml.Name{Local: "intValue"},
|
||||
Value: strconv.FormatInt(int64(int32(record.RecordId)), 10),
|
||||
}})
|
||||
continue
|
||||
}
|
||||
|
||||
var fieldValue *database.SakeField
|
||||
for name, value := range record.Fields {
|
||||
if name == field {
|
||||
fieldValue = &value
|
||||
break
|
||||
}
|
||||
}
|
||||
if fieldValue == nil {
|
||||
valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: nil})
|
||||
} else {
|
||||
value := fillValue(fieldValue.Type, fieldValue.Value)
|
||||
valueArray.RecordValues = append(valueArray.RecordValues, StorageRecordValue{Value: &value})
|
||||
}
|
||||
}
|
||||
response.ArrayOfRecordValue = append(response.ArrayOfRecordValue, valueArray)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func fillValue(valueType database.SakeFieldType, value string) StorageValue {
|
||||
return StorageValue{
|
||||
XMLName: xml.Name{Local: sakeTypeToTag[valueType]},
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (body *StorageResponseBody) setResultTag(xmlName string, result string) {
|
||||
switch xmlName {
|
||||
case SakeNamespace + "/GetMyRecords":
|
||||
body.GetMyRecordsResponse = &StorageGetMyRecordsResponse{
|
||||
GetMyRecordsResult: result,
|
||||
}
|
||||
|
||||
case SakeNamespace + "/UpdateRecord":
|
||||
body.UpdateRecordResponse = &StorageUpdateRecordResponse{
|
||||
UpdateRecordResult: result,
|
||||
}
|
||||
|
||||
case SakeNamespace + "/SearchForRecords":
|
||||
body.SearchForRecordsResponse = &StorageSearchForRecordsResponse{}
|
||||
}
|
||||
|
||||
logging.Info(moduleName, "Wrote", aurora.BrightCyan(fieldCount), "field(s) across", aurora.BrightCyan(i), "record(s)")
|
||||
return &response
|
||||
}
|
||||
|
|
|
|||
15
schema.sql
15
schema.sql
|
|
@ -63,6 +63,21 @@ END $$;
|
|||
|
||||
ALTER TABLE public.users OWNER TO wiilink;
|
||||
|
||||
--
|
||||
-- Name: sake_records; Type: TABLE; Schema: public; Owner: wiilink
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS public.sake_records (
|
||||
game_id integer NOT NULL,
|
||||
table_id character varying NOT NULL,
|
||||
record_id integer NOT NULL DEFAULT (random() * 2147483647)::integer,
|
||||
owner_id integer NOT NULL,
|
||||
fields jsonb NOT NULL,
|
||||
create_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT one_sake_record_constraint UNIQUE (game_id, table_id, record_id)
|
||||
);
|
||||
|
||||
--
|
||||
-- Name: mario_kart_wii_sake; Type: TABLE; Schema: public; Owner: wiilink
|
||||
--
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user