diff --git a/database/sake.go b/database/sake.go index 0efa096..a6a4f6b 100644 --- a/database/sake.go +++ b/database/sake.go @@ -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 +} diff --git a/sake/storage.go b/sake/storage.go index 37d3ffb..2bb61f7 100644 --- a/sake/storage.go +++ b/sake/storage.go @@ -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{} } diff --git a/schema.sql b/schema.sql index 4bb4250..b644edf 100644 --- a/schema.sql +++ b/schema.sql @@ -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,