SAKE: Limit records and fields per profile

This commit is contained in:
Palapeli 2026-04-01 13:47:41 -04:00
parent 2418fbf37d
commit 0a5e354e61
No known key found for this signature in database
GPG Key ID: 1FFE8F556A474925
3 changed files with 84 additions and 34 deletions

View File

@ -41,31 +41,37 @@ type SakeRecord struct {
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[]))
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
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
AND record_id = $3
RETURNING owner_id`
insertSakeRecordQuery = `
INSERT INTO sake_records (game_id, table_id, owner_id, fields)
VALUES ($1, $2, $3, $4)
RETURNING record_id`
checkMaxSakeRecordsQuery = `
SELECT COUNT(*)
FROM sake_records
WHERE owner_id = $1`
)
var (
ErrSakeNotOwned = errors.New("record is not owned by the specified owner ID")
ErrSakeNotOwned = errors.New("record is not owned by the specified owner ID")
ErrSakeFieldLimitExceeded = errors.New("record has too many fields")
)
func parseSakeFieldsFromJson(fieldsJson []byte) (map[string]SakeField, error) {
@ -146,6 +152,10 @@ func UpdateSakeRecord(pool *pgxpool.Pool, ctx context.Context, record SakeRecord
var existingOwnerId int32
err = pool.QueryRow(ctx, updateSakeRecordQuery, record.GameId, record.TableId, record.RecordId, ownerId, fieldsJson).Scan(&existingOwnerId)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.CheckViolation {
return ErrSakeFieldLimitExceeded
}
return err
}
if ownerId != 0 && existingOwnerId != ownerId {
@ -177,3 +187,12 @@ func InsertSakeRecord(pool *pgxpool.Pool, ctx context.Context, record SakeRecord
}
return recordId, err
}
func IsMaxSakeRecordsReached(pool *pgxpool.Pool, ctx context.Context, profileId uint32, maxRecords int) (bool, error) {
var count int
err := pool.QueryRow(ctx, checkMaxSakeRecordsQuery, profileId).Scan(&count)
if err != nil {
return false, err
}
return count >= maxRecords, nil
}

View File

@ -16,6 +16,12 @@ import (
"github.com/logrusorgru/aurora/v3"
)
const (
MaxSakeRecordsPerProfile = 96
MaxSakeFieldsPerRecord = 64
MaxSakeFieldValueLength = 4096
)
const (
ResultSuccess = "Success"
ResultSecretKeyInvalid = "SecretKeyInvalid"
@ -248,26 +254,19 @@ func getRequestIdentity(moduleName string, request StorageRequestData) (uint32,
return profileId, *gameInfo, ResultSuccess
}
func getInputFields(moduleName string, request StorageRequestData) (map[string]database.SakeField, string) {
record := database.SakeRecord{
Fields: make(map[string]database.SakeField),
}
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
}
if field.Name == "ownerid" || field.Name == "recordid" || field.Name == "gameid" || field.Name == "tableid" {
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 reached, err := database.IsMaxSakeRecordsReached(pool, ctx, profileId, MaxSakeRecordsPerProfile); err != nil {
logging.Error(moduleName, "Failed to check max sake records:", err)
return StorageResponseBody{CreateRecordResponse: &StorageCreateRecordResponse{
CreateRecordResult: ResultDatabaseUnavailable,
}}
} else if reached {
logging.Error(moduleName, "Profile", aurora.Cyan(profileId), "has reached the maximum number of sake records")
return StorageResponseBody{CreateRecordResponse: &StorageCreateRecordResponse{
CreateRecordResult: ResultRecordLimitReached,
}}
}
if request.TableID == "" {
logging.Error(moduleName, "No table ID provided")
return StorageResponseBody{CreateRecordResponse: &StorageCreateRecordResponse{
@ -489,6 +488,35 @@ func searchForRecords(moduleName string, profileId uint32, gameInfo common.GameI
return StorageResponseBody{SearchForRecordsResponse: &response}
}
func getInputFields(moduleName string, request StorageRequestData) (map[string]database.SakeField, string) {
record := database.SakeRecord{
Fields: make(map[string]database.SakeField),
}
if len(request.Values.RecordFields) > MaxSakeFieldsPerRecord {
logging.Error(moduleName, "Too many fields in record:", aurora.Cyan(len(request.Values.RecordFields)))
return nil, ResultFieldTypeInvalid
}
for _, field := range request.Values.RecordFields {
value := field.Value.Value.Value
if len(value) > MaxSakeFieldValueLength {
logging.Error(moduleName, "Field value too long for field", aurora.Cyan(field.Name), "with length", aurora.Cyan(len(value)))
return nil, ResultFieldTypeInvalid
}
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
}
if field.Name == "ownerid" || field.Name == "recordid" || field.Name == "gameid" || field.Name == "tableid" {
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 fillResponseValues(records []database.SakeRecord, request StorageRequestData) StorageResponseValues {
var response StorageResponseValues
for _, record := range records {
@ -537,16 +565,18 @@ func fillValue(valueType database.SakeFieldType, value string) StorageValue {
func (body *StorageResponseBody) setResultTag(xmlName string, result string) {
switch xmlName {
case SakeNamespace + "/GetMyRecords":
body.GetMyRecordsResponse = &StorageGetMyRecordsResponse{
GetMyRecordsResult: result,
case SakeNamespace + "/CreateRecord":
body.CreateRecordResponse = &StorageCreateRecordResponse{
CreateRecordResult: result,
}
case SakeNamespace + "/UpdateRecord":
body.UpdateRecordResponse = &StorageUpdateRecordResponse{
UpdateRecordResult: result,
}
case SakeNamespace + "/GetMyRecords":
body.GetMyRecordsResponse = &StorageGetMyRecordsResponse{
GetMyRecordsResult: result,
}
case SakeNamespace + "/SearchForRecords":
body.SearchForRecordsResponse = &StorageSearchForRecordsResponse{}
}

View File

@ -66,12 +66,13 @@ 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,
fields jsonb NOT NULL CHECK (jsonb_typeof(fields) = 'object' AND jsonb_array_length(fields) <= 64),
create_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
update_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP,