package web_api import ( "context" "errors" "fmt" "io" "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" "git.itzana.me/strafesnet/maps-service/pkg/service" ) var( CreationPhaseSubmissionsLimit = 20 CreationPhaseSubmissionStatuses = []model.SubmissionStatus{ model.SubmissionStatusChangesRequested, model.SubmissionStatusSubmitted, model.SubmissionStatusUnderConstruction, } // Allow 5 submissions every 10 minutes CreateSubmissionRateLimit int64 = 5 CreateSubmissionRecencyWindow = time.Second*600 ) var ( ErrCreationPhaseSubmissionsLimit = errors.New("Active submissions limited to 20") ErrUploadedAssetIDAlreadyExists = errors.New("The submission UploadedAssetID is already set") ErrReleaseInvalidStatus = errors.New("Only submissions with Uploaded status can be released") ErrReleaseNoUploadedAssetID = errors.New("Only submissions with a UploadedAssetID can be released") ErrAcceptOwnSubmission = fmt.Errorf("%w: You cannot accept your own submission as the submitter", ErrPermissionDenied) ErrCreateSubmissionRateLimit = errors.New("You must not create more than 5 submissions every 10 minutes") ) // POST /submissions func (svc *Service) CreateSubmission(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) { // sanitization if request.AssetID<0{ return nil, ErrNegativeID } var ModelID=uint64(request.AssetID); 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 submissions in the creation phase exceeds the limit { filter := service.NewSubmissionFilter() filter.SetSubmitter(userId) filter.SetStatuses(CreationPhaseSubmissionStatuses) creation_submissions, err := svc.inner.ListSubmissions(ctx, filter, model.Page{ Number: 1, Size: int32(CreationPhaseSubmissionsLimit), },datastore.ListSortDisabled) if err != nil { return nil, err } if CreationPhaseSubmissionsLimit <= len(creation_submissions) { return nil, ErrCreationPhaseSubmissionsLimit } } // Check if too many operations have been created recently { count, err := svc.inner.CountOperationsSince(ctx, int64(userId), time.Now().Add(-CreateSubmissionRecencyWindow), ) if err != nil { return nil, err } if CreateSubmissionRateLimit < count { return nil, ErrCreateSubmissionRateLimit } } operation, err := svc.inner.CreateOperation(ctx, model.Operation{ Owner: userId, StatusID: model.OperationStatusCreated, }) if err != nil { return nil, err } err = svc.inner.NatsCreateSubmission( operation.ID, ModelID, request.DisplayName, request.Creator, uint32(request.GameID), uint32(model.SubmissionStatusUnderConstruction), uint32(model.RolesEmpty), ) if err != nil { return nil, err } return &api.OperationID{ OperationID: operation.ID, }, nil } // POST /submissions-admin func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) { // sanitization if request.AssetID<0{ return nil, ErrNegativeID } var ModelID=uint64(request.AssetID); userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return nil, ErrUserInfo } userId, err := userInfo.GetUserID() if err != nil { return nil, err } roles, err := userInfo.GetRoles() if err != nil { return nil, err } // check if caller has required role has_role := roles & model.RolesSubmissionReview == model.RolesSubmissionReview if !has_role { return nil, ErrPermissionDeniedNeedRoleSubmissionReview } // Check if too many operations have been created recently { count, err := svc.inner.CountOperationsSince(ctx, int64(userId), time.Now().Add(-CreateSubmissionRecencyWindow), ) if err != nil { return nil, err } if CreateSubmissionRateLimit < count { return nil, ErrCreateSubmissionRateLimit } } operation, err := svc.inner.CreateOperation(ctx, model.Operation{ Owner: userId, StatusID: model.OperationStatusCreated, }) if err != nil { return nil, err } err = svc.inner.NatsCreateSubmission( operation.ID, ModelID, request.DisplayName, request.Creator, uint32(request.GameID), uint32(model.SubmissionStatusChangesRequested), uint32(roles), ) if err != nil { return nil, err } return &api.OperationID{ OperationID: operation.ID, }, nil } // GetSubmission implements getSubmission operation. // // Retrieve map with ID. // // GET /submissions/{SubmissionID} func (svc *Service) GetSubmission(ctx context.Context, params api.GetSubmissionParams) (*api.Submission, error) { submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return nil, err } return &api.Submission{ ID: submission.ID, DisplayName: submission.DisplayName, Creator: submission.Creator, GameID: int32(submission.GameID), CreatedAt: submission.CreatedAt.Unix(), UpdatedAt: submission.UpdatedAt.Unix(), Submitter: int64(submission.Submitter), AssetID: int64(submission.AssetID), AssetVersion: int64(submission.AssetVersion), Completed: submission.Completed, UploadedAssetID: api.NewOptInt64(int64(submission.UploadedAssetID)), StatusID: int32(submission.StatusID), }, nil } // ListSubmissions implements listSubmissions operation. // // Get list of submissions. // // GET /submissions func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissionsParams) (*api.Submissions, error) { filter := service.NewSubmissionFilter() if display_name, display_name_ok := params.DisplayName.Get(); display_name_ok{ filter.SetDisplayName(display_name) } if creator, creator_ok := params.Creator.Get(); creator_ok{ filter.SetCreator(creator) } if game_id, game_id_ok := params.GameID.Get(); game_id_ok{ filter.SetGameID(uint32(game_id)) } if submitter, submitter_ok := params.Submitter.Get(); submitter_ok{ filter.SetSubmitter(uint64(submitter)) } if asset_id, asset_id_ok := params.AssetID.Get(); asset_id_ok{ filter.SetAssetID(uint64(asset_id)) } if asset_version, asset_version_ok := params.AssetVersion.Get(); asset_version_ok{ filter.SetAssetVersion(uint64(asset_version)) } if uploaded_asset_id, uploaded_asset_id_ok := params.UploadedAssetID.Get(); uploaded_asset_id_ok{ filter.SetUploadedAssetID(uint64(uploaded_asset_id)) } if status_id, status_id_ok := params.StatusID.Get(); status_id_ok{ filter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatus(status_id)}) } sort := datastore.ListSort(params.Sort.Or(int32(datastore.ListSortDisabled))) total, items, err := svc.inner.ListSubmissionsWithTotal(ctx, filter, model.Page{ Number: params.Page, Size: params.Limit, },sort) if err != nil { return nil, err } var resp api.Submissions resp.Total=total for _, item := range items { resp.Submissions = append(resp.Submissions, api.Submission{ ID: item.ID, DisplayName: item.DisplayName, Creator: item.Creator, GameID: int32(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, UploadedAssetID: api.NewOptInt64(int64(item.UploadedAssetID)), StatusID: int32(item.StatusID), }) } return &resp, nil } // PatchSubmissionCompleted implements patchSubmissionCompleted operation. // // Retrieve map with ID. // // POST /submissions/{SubmissionID}/completed func (svc *Service) SetSubmissionCompleted(ctx context.Context, params api.SetSubmissionCompletedParams) 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 } update := service.NewSubmissionUpdate() update.SetCompleted(true) return svc.inner.UpdateSubmission(ctx, params.SubmissionID, update) } // UpdateSubmissionModel implements patchSubmissionModel operation. // // Update model following role restrictions. // // POST /submissions/{SubmissionID}/model func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.UpdateSubmissionModelParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } // read submission (this could be done with a transaction WHERE clause) submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } userId, err := userInfo.GetUserID() if err != nil { return err } // check if caller is the submitter has_role := userId == submission.Submitter if !has_role { return ErrPermissionDeniedNotSubmitter } OldModelID := submission.AssetID OldModelVersion := submission.AssetVersion NewModelID := uint64(params.ModelID) NewModelVersion := uint64(params.ModelVersion) // check if Status is ChangesRequested|Submitted|UnderConstruction update := service.NewSubmissionUpdate() update.SetAssetID(NewModelID) update.SetAssetVersion(NewModelVersion) //always reset completed when model changes update.SetCompleted(false) err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, []model.SubmissionStatus{model.SubmissionStatusChangesRequested, model.SubmissionStatusUnderConstruction}, update) if err != nil { return err } event_data := model.AuditEventDataChangeModel{ OldModelID: OldModelID, OldModelVersion: OldModelVersion, NewModelID: NewModelID, NewModelVersion: NewModelVersion, } return svc.inner.CreateAuditEventChangeModel( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionReject invokes actionSubmissionReject operation. // // Role Reviewer changes status from Submitted -> Rejected. // // POST /submissions/{SubmissionID}/status/reject func (svc *Service) ActionSubmissionReject(ctx context.Context, params api.ActionSubmissionRejectParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } // check if caller has required role if !has_role { return ErrPermissionDeniedNeedRoleSubmissionReview } userId, err := userInfo.GetUserID() if err != nil { return err } // transaction target_status := model.SubmissionStatusRejected update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusSubmitted} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionRequestChanges invokes actionSubmissionRequestChanges operation. // // Role Reviewer changes status from Validated|Accepted|Submitted -> ChangesRequested. // // POST /submissions/{SubmissionID}/status/request-changes func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params api.ActionSubmissionRequestChangesParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } // check if caller has required role if !has_role { return ErrPermissionDeniedNeedRoleSubmissionReview } userId, err := userInfo.GetUserID() if err != nil { return err } // transaction target_status := model.SubmissionStatusChangesRequested update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusValidated, model.SubmissionStatusAcceptedUnvalidated, model.SubmissionStatusSubmitted} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionRevoke invokes actionSubmissionRevoke operation. // // Role Submitter changes status from Submitted|ChangesRequested -> UnderConstruction. // // POST /submissions/{SubmissionID}/status/revoke func (svc *Service) ActionSubmissionRevoke(ctx context.Context, params api.ActionSubmissionRevokeParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } // read submission (this could be done with a transaction WHERE clause) submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } userId, err := userInfo.GetUserID() if err != nil { return err } // check if caller is the submitter has_role := userId == submission.Submitter if !has_role { return ErrPermissionDeniedNotSubmitter } // transaction target_status := model.SubmissionStatusUnderConstruction update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusSubmitted, model.SubmissionStatusChangesRequested} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionTriggerSubmit invokes actionSubmissionTriggerSubmit operation. // // Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting. // // POST /submissions/{SubmissionID}/status/trigger-submit func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params api.ActionSubmissionTriggerSubmitParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } // read submission (this could be done with a transaction WHERE clause) submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } userId, err := userInfo.GetUserID() if err != nil { return err } // check if caller is the submitter is_submitter := userId == submission.Submitter // neither = deny if !is_submitter { has_submission_review, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } if !has_submission_review { return ErrPermissionDeniedNotSubmitter } } // transaction target_status := model.SubmissionStatusSubmitting update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusUnderConstruction, model.SubmissionStatusChangesRequested} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } err = svc.inner.NatsCheckSubmission( submission.ID, submission.AssetID, false, ) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionTriggerSubmitUnchecked invokes actionSubmissionTriggerSubmitUnchecked operation. // // Role Reviewer changes status from ChangesRequested -> Submitting. // // POST /submissions/{SubmissionID}/status/trigger-submit-unchecked func (svc *Service) ActionSubmissionTriggerSubmitUnchecked(ctx context.Context, params api.ActionSubmissionTriggerSubmitUncheckedParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } // read submission (this could be done with a transaction WHERE clause) submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } userId, err := userInfo.GetUserID() if err != nil { return err } // check if caller is the submitter is_submitter := userId == submission.Submitter if is_submitter { return ErrAcceptOwnSubmission } has_submission_review, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } if !has_submission_review { return ErrPermissionDeniedNeedRoleSubmissionReview } // transaction target_status := model.SubmissionStatusSubmitting update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusChangesRequested} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } err = svc.inner.NatsCheckSubmission( submission.ID, submission.AssetID, true, ) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionResetSubmitting implements actionSubmissionResetSubmitting operation. // // Role SubmissionReview changes status from Submitting -> UnderConstruction. // // POST /submissions/{SubmissionID}/status/reset-submitting func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params api.ActionSubmissionResetSubmittingParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } userId, err := userInfo.GetUserID() if err != nil { return err } // check when submission was updated submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) { // the last time the submission was updated must be longer than 10 seconds ago return ErrDelayReset } // check if caller has required role has_role := userId == submission.Submitter if !has_role { return ErrPermissionDeniedNotSubmitter } // transaction target_status := model.SubmissionStatusUnderConstruction update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusSubmitting} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionTriggerUpload invokes actionSubmissionTriggerUpload operation. // // Role Admin changes status from Validated -> Uploading. // // POST /submissions/{SubmissionID}/status/trigger-upload func (svc *Service) ActionSubmissionTriggerUpload(ctx context.Context, params api.ActionSubmissionTriggerUploadParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionUpload() if err != nil { return err } // check if caller has required role if !has_role { return ErrPermissionDeniedNeedRoleSubmissionUpload } userId, err := userInfo.GetUserID() if err != nil { return err } // transaction target_status := model.SubmissionStatusUploading update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusValidated} submission, err := svc.inner.UpdateAndGetSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } // sentinel value because we are not using rust if submission.UploadedAssetID == 0 { // this is a new map err = svc.inner.NatsUploadSubmission( submission.ID, submission.ValidatedAssetID, submission.ValidatedAssetVersion, // upload ModelName as displayname, whatever submission.DisplayName, ) if err != nil { return err } } else { // refuse to operate return ErrUploadedAssetIDAlreadyExists } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionValidate invokes actionSubmissionValidate operation. // // Role SubmissionRelease changes status from Uploading -> Validated. // // POST /submissions/{SubmissionID}/status/reset-uploading func (svc *Service) ActionSubmissionValidated(ctx context.Context, params api.ActionSubmissionValidatedParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionUpload() if err != nil { return err } // check if caller has required role if !has_role { return ErrPermissionDeniedNeedRoleSubmissionUpload } userId, err := userInfo.GetUserID() if err != nil { return err } // check when submission was updated submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) { // the last time the submission was updated must be longer than 10 seconds ago return ErrDelayReset } // transaction target_status := model.SubmissionStatusValidated update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusUploading} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionTriggerValidate invokes actionSubmissionTriggerValidate operation. // // Role Reviewer triggers validation and changes status from Submitted -> Validating. // // POST /submissions/{SubmissionID}/status/trigger-validate func (svc *Service) ActionSubmissionTriggerValidate(ctx context.Context, params api.ActionSubmissionTriggerValidateParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } // check if caller has required role if !has_role { return ErrPermissionDeniedNeedRoleSubmissionReview } // read submission (this could be done with a transaction WHERE clause) submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } userId, err := userInfo.GetUserID() if err != nil { return err } // check if caller is NOT the submitter has_role = userId == submission.Submitter if has_role { return ErrAcceptOwnSubmission } // transaction target_status := model.SubmissionStatusValidating update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusSubmitted} submission, err = svc.inner.UpdateAndGetSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } err = svc.inner.NatsValidateSubmission( submission.ID, submission.AssetID, submission.AssetVersion, submission.ValidatedAssetID, ) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionRetryValidate invokes actionSubmissionRetryValidate operation. // // Role Reviewer re-runs validation and changes status from Accepted -> Validating. // // POST /submissions/{SubmissionID}/status/retry-validate func (svc *Service) ActionSubmissionRetryValidate(ctx context.Context, params api.ActionSubmissionRetryValidateParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } // check if caller has required role if !has_role { return ErrPermissionDeniedNeedRoleSubmissionReview } userId, err := userInfo.GetUserID() if err != nil { return err } // transaction target_status := model.SubmissionStatusValidating update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusAcceptedUnvalidated} submission, err := svc.inner.UpdateAndGetSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } err = svc.inner.NatsValidateSubmission( submission.ID, submission.AssetID, submission.AssetVersion, submission.ValidatedAssetID, ) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ActionSubmissionAccepted implements actionSubmissionAccepted operation. // // Role SubmissionReview changes status from Validating -> Accepted. // // POST /submissions/{SubmissionID}/status/reset-validating func (svc *Service) ActionSubmissionAccepted(ctx context.Context, params api.ActionSubmissionAcceptedParams) error { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } // check if caller has required role if !has_role { return ErrPermissionDeniedNeedRoleSubmissionReview } userId, err := userInfo.GetUserID() if err != nil { return err } // check when submission was updated submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) { // the last time the submission was updated must be longer than 10 seconds ago return ErrDelayReset } // transaction target_status := model.SubmissionStatusAcceptedUnvalidated update := service.NewSubmissionUpdate() update.SetStatusID(target_status) allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusValidating} err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update) if err != nil { return err } event_data := model.AuditEventDataAction{ TargetStatus: uint32(target_status), } return svc.inner.CreateAuditEventAction( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ReleaseSubmissions invokes releaseSubmissions operation. // // Release a set of uploaded maps. // // POST /release-submissions func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.ReleaseInfo) (*api.OperationID, error) { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return nil, ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionRelease() if err != nil { return nil, err } // check if caller has required role if !has_role { return nil, ErrPermissionDeniedNeedRoleSubmissionRelease } userId, err := userInfo.GetUserID() if err != nil { return nil, err } idList := make([]int64, len(request)) for i, releaseInfo := range request { idList[i] = releaseInfo.SubmissionID } // fetch submissions submissions, err := svc.inner.GetSubmissionList(ctx, idList) if err != nil { return nil, err } // the submissions are not ordered the same as the idList! id_to_submission := make(map[int64]*model.Submission, len(submissions)) // check each submission to make sure it is ready to release for _,submission := range submissions{ if submission.StatusID != model.SubmissionStatusUploaded{ return nil, ErrReleaseInvalidStatus } if submission.UploadedAssetID == 0{ return nil, ErrReleaseNoUploadedAssetID } id_to_submission[submission.ID] = &submission } // construct batch release nats message release_submissions := make([]model.ReleaseSubmissionRequest, len(request)) for i, release_info := range request { // from request release_submissions[i].ReleaseDate = release_info.Date.Unix() release_submissions[i].SubmissionID = release_info.SubmissionID submission := id_to_submission[release_info.SubmissionID] // from submission release_submissions[i].ModelID = submission.ValidatedAssetID release_submissions[i].ModelVersion = submission.ValidatedAssetVersion // for map create release_submissions[i].UploadedAssetID = submission.UploadedAssetID release_submissions[i].DisplayName = submission.DisplayName release_submissions[i].Creator = submission.Creator release_submissions[i].GameID = submission.GameID release_submissions[i].Submitter = submission.Submitter } // create a trackable long-running operation operation, err := svc.inner.CreateOperation(ctx, model.Operation{ Owner: userId, StatusID: model.OperationStatusCreated, }) if err != nil { return nil, err } // this is a map fix err = svc.inner.NatsBatchReleaseSubmissions( release_submissions, operation.ID, ) if err != nil { return nil, err } return &api.OperationID{ OperationID: operation.ID, }, nil } // CreateSubmissionAuditComment implements createSubmissionAuditComment operation. // // Post a comment to the audit log // // POST /submissions/{SubmissionID}/comment func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.CreateSubmissionAuditCommentReq, params api.CreateSubmissionAuditCommentParams) (error) { userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) if !ok { return ErrUserInfo } has_role, err := userInfo.HasRoleSubmissionReview() if err != nil { return err } userId, err := userInfo.GetUserID() if err != nil { return err } if !has_role { // Submitter has special permission to comment on their submission submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID) if err != nil { return err } if submission.Submitter != userId { return ErrPermissionDeniedNeedRoleSubmissionReview } } data, err := io.ReadAll(req) if err != nil { return err } event_data := model.AuditEventDataComment{ Comment: string(data), } return svc.inner.CreateAuditEventComment( ctx, userId, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, event_data, ) } // ListSubmissionAuditEvents invokes listSubmissionAuditEvents operation. // // Retrieve a list of audit events. // // GET /submissions/{SubmissionID}/audit-events func (svc *Service) ListSubmissionAuditEvents(ctx context.Context, params api.ListSubmissionAuditEventsParams) ([]api.AuditEvent, error) { return svc.inner.ListAuditEvents( ctx, model.Resource{ ID: params.SubmissionID, Type: model.ResourceSubmission, }, model.Page{ Number: params.Page, Size: params.Limit, }, ) }