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.MapfixCreate) (*api.ID, error) { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return nil, ErrUserInfo } userId, err := userInfo.GetUserID() if err != nil { return nil, err } // Check if user's mapfixes in the creation phase exceeds the limit { filter := datastore.Optional() filter.Add("submitter", int64(userId)) filter.Add("status_id", CreationPhaseMapfixStatuses) creation_mapfixes, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{ Number: 1, Size: int32(CreationPhaseMapfixesLimit), },datastore.ListSortDisabled) if err != nil { return nil, err } if CreationPhaseMapfixesLimit <= len(creation_mapfixes) { return nil, ErrCreationPhaseMapfixesLimit } } // Check if an active mapfix with the same asset id exists { filter := datastore.Optional() filter.Add("asset_id", request.AssetID) filter.Add("asset_version", request.AssetVersion) filter.Add("status_id", ActiveMapfixStatuses) active_mapfixes, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{ Number: 1, Size: 1, },datastore.ListSortDisabled) if err != nil { return nil, err } if len(active_mapfixes) != 0{ return nil, ErrActiveMapfixSameAssetID } } mapfix, err := svc.DB.Mapfixes().Create(ctx, model.Mapfix{ ID: 0, DisplayName: request.DisplayName, Creator: request.Creator, GameID: request.GameID, Submitter: int64(userId), AssetID: request.AssetID, AssetVersion: request.AssetVersion, Completed: false, TargetAssetID: request.TargetAssetID, StatusID: model.MapfixStatusUnderConstruction, }) if err != nil { return nil, err } return &api.ID{ ID: mapfix.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) }