588 lines
17 KiB
Go
588 lines
17 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
|
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
|
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
|
)
|
|
|
|
var(
|
|
CreationPhaseMapfixesLimit = 20
|
|
CreationPhaseMapfixStatuses = []model.MapfixStatus{
|
|
model.MapfixStatusChangesRequested,
|
|
model.MapfixStatusSubmitted,
|
|
model.MapfixStatusUnderConstruction,
|
|
}
|
|
// prevent two mapfixes with same asset id
|
|
ActiveMapfixStatuses = []model.MapfixStatus{
|
|
model.MapfixStatusUploading,
|
|
model.MapfixStatusValidated,
|
|
model.MapfixStatusValidating,
|
|
model.MapfixStatusAccepted,
|
|
model.MapfixStatusChangesRequested,
|
|
model.MapfixStatusSubmitted,
|
|
model.MapfixStatusUnderConstruction,
|
|
}
|
|
// limit mapfixes in the pipeline to one per target map
|
|
ActiveAcceptedMapfixStatuses = []model.MapfixStatus{
|
|
model.MapfixStatusUploading,
|
|
model.MapfixStatusValidated,
|
|
model.MapfixStatusValidating,
|
|
model.MapfixStatusAccepted,
|
|
}
|
|
)
|
|
|
|
var (
|
|
ErrCreationPhaseMapfixesLimit = errors.New("Active mapfixes limited to 20")
|
|
ErrActiveMapfixSameAssetID = errors.New("There is an active mapfix with the same AssetID")
|
|
ErrActiveMapfixSameTargetAssetID = errors.New("There is an active mapfix with the same TargetAssetID")
|
|
ErrAcceptOwnMapfix = fmt.Errorf("%w: You cannot accept your own mapfix as the submitter", ErrPermissionDenied)
|
|
)
|
|
|
|
// POST /mapfixes
|
|
func (svc *Service) CreateMapfix(ctx context.Context, request *api.MapfixTriggerCreate) (*api.OperationID, error) {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return nil, ErrUserInfo
|
|
}
|
|
|
|
userId, err := userInfo.GetUserID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
operation, err := svc.DB.Operations().Create(ctx, model.Operation{
|
|
Owner: int64(userId),
|
|
StatusID: model.OperationStatusCreated,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
create_request := model.CreateMapfixRequest{
|
|
OperationID: operation.ID,
|
|
ModelID: request.AssetID,
|
|
TargetAssetID: request.TargetAssetID,
|
|
}
|
|
|
|
j, err := json.Marshal(create_request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
svc.Nats.Publish("maptest.mapfixes.create", []byte(j))
|
|
|
|
return &api.OperationID{
|
|
OperationID: operation.ID,
|
|
}, nil
|
|
}
|
|
|
|
// GetMapfix implements getMapfix operation.
|
|
//
|
|
// Retrieve map with ID.
|
|
//
|
|
// GET /mapfixes/{MapfixID}
|
|
func (svc *Service) GetMapfix(ctx context.Context, params api.GetMapfixParams) (*api.Mapfix, error) {
|
|
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &api.Mapfix{
|
|
ID: mapfix.ID,
|
|
DisplayName: mapfix.DisplayName,
|
|
Creator: mapfix.Creator,
|
|
GameID: mapfix.GameID,
|
|
CreatedAt: mapfix.CreatedAt.Unix(),
|
|
UpdatedAt: mapfix.UpdatedAt.Unix(),
|
|
Submitter: int64(mapfix.Submitter),
|
|
AssetID: int64(mapfix.AssetID),
|
|
AssetVersion: int64(mapfix.AssetVersion),
|
|
Completed: mapfix.Completed,
|
|
TargetAssetID: int64(mapfix.TargetAssetID),
|
|
StatusID: int32(mapfix.StatusID),
|
|
StatusMessage: mapfix.StatusMessage,
|
|
}, nil
|
|
}
|
|
|
|
// ListMapfixes implements listMapfixes operation.
|
|
//
|
|
// Get list of mapfixes.
|
|
//
|
|
// GET /mapfixes
|
|
func (svc *Service) ListMapfixes(ctx context.Context, params api.ListMapfixesParams) ([]api.Mapfix, error) {
|
|
filter := datastore.Optional()
|
|
|
|
if params.DisplayName.IsSet(){
|
|
filter.Add("display_name", params.DisplayName.Value)
|
|
}
|
|
if params.Creator.IsSet(){
|
|
filter.Add("creator", params.Creator.Value)
|
|
}
|
|
if params.GameID.IsSet(){
|
|
filter.Add("game_id", params.GameID.Value)
|
|
}
|
|
|
|
sort := datastore.ListSort(params.Sort.Or(int32(datastore.ListSortDisabled)))
|
|
|
|
items, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{
|
|
Number: params.Page,
|
|
Size: params.Limit,
|
|
},sort)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var resp []api.Mapfix
|
|
for _, item := range items {
|
|
resp = append(resp, api.Mapfix{
|
|
ID: item.ID,
|
|
DisplayName: item.DisplayName,
|
|
Creator: item.Creator,
|
|
GameID: item.GameID,
|
|
CreatedAt: item.CreatedAt.Unix(),
|
|
UpdatedAt: item.UpdatedAt.Unix(),
|
|
Submitter: int64(item.Submitter),
|
|
AssetID: int64(item.AssetID),
|
|
AssetVersion: int64(item.AssetVersion),
|
|
Completed: item.Completed,
|
|
TargetAssetID: int64(item.TargetAssetID),
|
|
StatusID: int32(item.StatusID),
|
|
})
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// PatchMapfixCompleted implements patchMapfixCompleted operation.
|
|
//
|
|
// Retrieve map with ID.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/completed
|
|
func (svc *Service) SetMapfixCompleted(ctx context.Context, params api.SetMapfixCompletedParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMaptest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has MaptestGame role (request must originate from a maptest roblox game)
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMaptest
|
|
}
|
|
|
|
pmap := datastore.Optional()
|
|
pmap.Add("completed", true)
|
|
return svc.DB.Mapfixes().Update(ctx, params.MapfixID, pmap)
|
|
}
|
|
|
|
// UpdateMapfixModel implements patchMapfixModel operation.
|
|
//
|
|
// Update model following role restrictions.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/model
|
|
func (svc *Service) UpdateMapfixModel(ctx context.Context, params api.UpdateMapfixModelParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
// read mapfix (this could be done with a transaction WHERE clause)
|
|
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
has_role, err := userInfo.IsSubmitter(uint64(mapfix.Submitter))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller is the submitter
|
|
if !has_role {
|
|
return ErrPermissionDeniedNotSubmitter
|
|
}
|
|
|
|
// check if Status is ChangesRequested|Submitted|UnderConstruction
|
|
pmap := datastore.Optional()
|
|
pmap.AddNotNil("asset_id", params.ModelID)
|
|
pmap.AddNotNil("asset_version", params.VersionID)
|
|
//always reset completed when model changes
|
|
pmap.Add("completed", false)
|
|
return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusSubmitted, model.MapfixStatusUnderConstruction}, pmap)
|
|
}
|
|
|
|
// ActionMapfixReject invokes actionMapfixReject operation.
|
|
//
|
|
// Role Reviewer changes status from Submitted -> Rejected.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/reject
|
|
func (svc *Service) ActionMapfixReject(ctx context.Context, params api.ActionMapfixRejectParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMapfixReview()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has required role
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMapReview
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusRejected)
|
|
return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitted}, smap)
|
|
}
|
|
|
|
// ActionMapfixRequestChanges invokes actionMapfixRequestChanges operation.
|
|
//
|
|
// Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/request-changes
|
|
func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params api.ActionMapfixRequestChangesParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMapfixReview()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has required role
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMapReview
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusChangesRequested)
|
|
return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAccepted, model.MapfixStatusSubmitted}, smap)
|
|
}
|
|
|
|
// ActionMapfixRevoke invokes actionMapfixRevoke operation.
|
|
//
|
|
// Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/revoke
|
|
func (svc *Service) ActionMapfixRevoke(ctx context.Context, params api.ActionMapfixRevokeParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
// read mapfix (this could be done with a transaction WHERE clause)
|
|
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
has_role, err := userInfo.IsSubmitter(uint64(mapfix.Submitter))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller is the submitter
|
|
if !has_role {
|
|
return ErrPermissionDeniedNotSubmitter
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusUnderConstruction)
|
|
return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitted, model.MapfixStatusChangesRequested}, smap)
|
|
}
|
|
|
|
// ActionMapfixSubmit invokes actionMapfixSubmit operation.
|
|
//
|
|
// Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitted.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/submit
|
|
func (svc *Service) ActionMapfixSubmit(ctx context.Context, params api.ActionMapfixSubmitParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
// read mapfix (this could be done with a transaction WHERE clause)
|
|
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
has_role, err := userInfo.IsSubmitter(uint64(mapfix.Submitter))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller is the submitter
|
|
if !has_role {
|
|
return ErrPermissionDeniedNotSubmitter
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusSubmitted)
|
|
return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusUnderConstruction, model.MapfixStatusChangesRequested}, smap)
|
|
}
|
|
|
|
// ActionMapfixTriggerUpload invokes actionMapfixTriggerUpload operation.
|
|
//
|
|
// Role Admin changes status from Validated -> Uploading.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/trigger-upload
|
|
func (svc *Service) ActionMapfixTriggerUpload(ctx context.Context, params api.ActionMapfixTriggerUploadParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMapfixUpload()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has required role
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMapUpload
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusUploading)
|
|
mapfix, err := svc.DB.Mapfixes().IfStatusThenUpdateAndGet(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidated}, smap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// this is a map fix
|
|
upload_fix_request := model.UploadMapfixRequest{
|
|
MapfixID: mapfix.ID,
|
|
ModelID: mapfix.ValidatedAssetID,
|
|
ModelVersion: mapfix.ValidatedAssetVersion,
|
|
TargetAssetID: mapfix.TargetAssetID,
|
|
}
|
|
|
|
j, err := json.Marshal(upload_fix_request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
svc.Nats.Publish("maptest.mapfixes.uploadfix", []byte(j))
|
|
|
|
return nil
|
|
}
|
|
|
|
// ActionMapfixValidate invokes actionMapfixValidate operation.
|
|
//
|
|
// Role MapfixRelease changes status from Uploading -> Validated.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/reset-uploading
|
|
func (svc *Service) ActionMapfixValidated(ctx context.Context, params api.ActionMapfixValidatedParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMapfixUpload()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has required role
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMapUpload
|
|
}
|
|
|
|
// check when mapfix was updated
|
|
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if time.Now().Before(mapfix.UpdatedAt.Add(time.Second*10)) {
|
|
// the last time the mapfix was updated must be longer than 10 seconds ago
|
|
return ErrDelayReset
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusValidated)
|
|
return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusUploading}, smap)
|
|
}
|
|
|
|
// ActionMapfixTriggerValidate invokes actionMapfixTriggerValidate operation.
|
|
//
|
|
// Role Reviewer triggers validation and changes status from Submitted -> Validating.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/trigger-validate
|
|
func (svc *Service) ActionMapfixTriggerValidate(ctx context.Context, params api.ActionMapfixTriggerValidateParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMapfixReview()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has required role
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMapReview
|
|
}
|
|
|
|
// read mapfix (this could be done with a transaction WHERE clause)
|
|
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
has_role, err = userInfo.IsSubmitter(uint64(mapfix.Submitter))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller is NOT the submitter
|
|
if has_role {
|
|
return ErrAcceptOwnMapfix
|
|
}
|
|
|
|
// Check if an active mapfix with the same target asset id exists
|
|
if mapfix.TargetAssetID != 0 {
|
|
filter := datastore.Optional()
|
|
filter.Add("target_asset_id", mapfix.TargetAssetID)
|
|
filter.Add("status_id", ActiveAcceptedMapfixStatuses)
|
|
active_mapfixes, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{
|
|
Number: 1,
|
|
Size: 1,
|
|
},datastore.ListSortDisabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(active_mapfixes) != 0{
|
|
return ErrActiveMapfixSameTargetAssetID
|
|
}
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusValidating)
|
|
mapfix, err = svc.DB.Mapfixes().IfStatusThenUpdateAndGet(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusSubmitted}, smap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
validate_request := model.ValidateMapfixRequest{
|
|
MapfixID: mapfix.ID,
|
|
ModelID: mapfix.AssetID,
|
|
ModelVersion: mapfix.AssetVersion,
|
|
ValidatedModelID: nil,
|
|
}
|
|
|
|
// sentinel values because we're not using rust
|
|
if mapfix.ValidatedAssetID != 0 {
|
|
validate_request.ValidatedModelID = &mapfix.ValidatedAssetID
|
|
}
|
|
|
|
j, err := json.Marshal(validate_request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
svc.Nats.Publish("maptest.mapfixes.validate", []byte(j))
|
|
|
|
return nil
|
|
}
|
|
|
|
// ActionMapfixRetryValidate invokes actionMapfixRetryValidate operation.
|
|
//
|
|
// Role Reviewer re-runs validation and changes status from Accepted -> Validating.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/retry-validate
|
|
func (svc *Service) ActionMapfixRetryValidate(ctx context.Context, params api.ActionMapfixRetryValidateParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMapfixReview()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has required role
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMapReview
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusValidating)
|
|
mapfix, err := svc.DB.Mapfixes().IfStatusThenUpdateAndGet(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusAccepted}, smap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
validate_request := model.ValidateMapfixRequest{
|
|
MapfixID: mapfix.ID,
|
|
ModelID: mapfix.AssetID,
|
|
ModelVersion: mapfix.AssetVersion,
|
|
ValidatedModelID: nil,
|
|
}
|
|
|
|
// sentinel values because we're not using rust
|
|
if mapfix.ValidatedAssetID != 0 {
|
|
validate_request.ValidatedModelID = &mapfix.ValidatedAssetID
|
|
}
|
|
|
|
j, err := json.Marshal(validate_request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
svc.Nats.Publish("maptest.mapfixes.validate", []byte(j))
|
|
|
|
return nil
|
|
}
|
|
|
|
// ActionMapfixAccepted implements actionMapfixAccepted operation.
|
|
//
|
|
// Role MapfixReview changes status from Validating -> Accepted.
|
|
//
|
|
// POST /mapfixes/{MapfixID}/status/reset-validating
|
|
func (svc *Service) ActionMapfixAccepted(ctx context.Context, params api.ActionMapfixAcceptedParams) error {
|
|
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
|
if !ok {
|
|
return ErrUserInfo
|
|
}
|
|
|
|
has_role, err := userInfo.HasRoleMapfixReview()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// check if caller has required role
|
|
if !has_role {
|
|
return ErrPermissionDeniedNeedRoleMapReview
|
|
}
|
|
|
|
// check when mapfix was updated
|
|
mapfix, err := svc.DB.Mapfixes().Get(ctx, params.MapfixID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if time.Now().Before(mapfix.UpdatedAt.Add(time.Second*10)) {
|
|
// the last time the mapfix was updated must be longer than 10 seconds ago
|
|
return ErrDelayReset
|
|
}
|
|
|
|
// transaction
|
|
smap := datastore.Optional()
|
|
smap.Add("status_id", model.MapfixStatusAccepted)
|
|
smap.Add("status_message", "Manually forced reset")
|
|
return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusValidating}, smap)
|
|
}
|