From 468204b29923e98d0537758c3c841947002aa7c6 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 12 Aug 2025 15:48:59 -0700 Subject: [PATCH 01/27] validator: dedicated api key for luau execution --- validation/src/main.rs | 6 +++++- validation/src/message_handler.rs | 1 + validation/src/upload_mapfix.rs | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/validation/src/main.rs b/validation/src/main.rs index e472e78..9c58329 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -53,9 +53,12 @@ async fn main()->Result<(),StartupError>{ // create / upload models through STRAFESNET_CI2 account let cookie=std::env::var("RBXCOOKIE").expect("RBXCOOKIE env required"); let cookie_context=rbx_asset::cookie::Context::new(rbx_asset::cookie::Cookie::new(cookie)); - // download models through cloud api + // download models through cloud api (STRAFESNET_CI2 account) let api_key=std::env::var("RBX_API_KEY").expect("RBX_API_KEY env required"); let cloud_context=rbx_asset::cloud::Context::new(rbx_asset::cloud::ApiKey::new(api_key)); + // luau execution cloud api (StrafesNET group) + let api_key=std::env::var("RBX_API_KEY_LUAU_EXECUTION").expect("RBX_API_KEY_LUAU_EXECUTION env required"); + let cloud_context_luau_execution=rbx_asset::cloud::Context::new(rbx_asset::cloud::ApiKey::new(api_key)); // maps-service api let api_host_internal=std::env::var("API_HOST_INTERNAL").expect("API_HOST_INTERNAL env required"); @@ -70,6 +73,7 @@ async fn main()->Result<(),StartupError>{ let message_handler=message_handler::MessageHandler{ cloud_context, cookie_context, + cloud_context_luau_execution, group_id, load_asset_version_place_id, load_asset_version_universe_id, diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 1404476..7e5a046 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -30,6 +30,7 @@ fn from_slice<'a,T:serde::de::Deserialize<'a>>(slice:&'a [u8])->Result, pub(crate) load_asset_version_place_id:u64, pub(crate) load_asset_version_universe_id:u64, diff --git a/validation/src/upload_mapfix.rs b/validation/src/upload_mapfix.rs index f9b32ca..b0d0e27 100644 --- a/validation/src/upload_mapfix.rs +++ b/validation/src/upload_mapfix.rs @@ -93,9 +93,9 @@ impl crate::message_handler::MessageHandler{ enableBinaryOutput:None, binaryOutputUri:None, }; - let session_response=self.cloud_context.create_luau_session(&request,session).await.map_err(Error::CreateSession)?; + let session_response=self.cloud_context_luau_execution.create_luau_session(&request,session).await.map_err(Error::CreateSession)?; - let result=crate::rbx_util::get_luau_result_exp_backoff(&self.cloud_context,&session_response).await; + let result=crate::rbx_util::get_luau_result_exp_backoff(&self.cloud_context_luau_execution,&session_response).await; let load_asset_version=match result{ Ok(Ok(rbx_asset::cloud::LuauResults{results}))=>match results.as_slice(){ From a62a231b0a664f5e92b7dcc479df5b368ef486c1 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 12 Aug 2025 16:05:13 -0700 Subject: [PATCH 02/27] backend: new statuses for mapfix and submission --- pkg/model/mapfix.go | 4 +++- pkg/model/submission.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/model/mapfix.go b/pkg/model/mapfix.go index 950c6ec..00e63c3 100644 --- a/pkg/model/mapfix.go +++ b/pkg/model/mapfix.go @@ -18,10 +18,12 @@ const ( MapfixStatusValidating MapfixStatus = 5 MapfixStatusValidated MapfixStatus = 6 MapfixStatusUploading MapfixStatus = 7 + MapfixStatusUploaded MapfixStatus = 8 // uploaded to the group, but pending release + MapfixStatusReleasing MapfixStatus = 11 // Phase: Final MapfixStatus - MapfixStatusUploaded MapfixStatus = 8 // uploaded to the group, but pending release MapfixStatusRejected MapfixStatus = 9 + MapfixStatusReleased MapfixStatus = 10 ) type Mapfix struct { diff --git a/pkg/model/submission.go b/pkg/model/submission.go index 4840e5e..2b559a9 100644 --- a/pkg/model/submission.go +++ b/pkg/model/submission.go @@ -19,6 +19,7 @@ const ( SubmissionStatusValidated SubmissionStatus = 6 SubmissionStatusUploading SubmissionStatus = 7 SubmissionStatusUploaded SubmissionStatus = 8 // uploaded to the group, but pending release + SubmissionStatusReleasing SubmissionStatus = 11 // Phase: Final SubmissionStatus SubmissionStatusRejected SubmissionStatus = 9 From 25f6c9e086375c67a69d27bd3162ecf87746bf8f Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 12 Aug 2025 16:05:33 -0700 Subject: [PATCH 03/27] backend: tweak status sets to reflect new statuses --- pkg/validator_controller/mapfixes.go | 2 ++ pkg/validator_controller/submissions.go | 4 +++- pkg/web_api/mapfixes.go | 2 ++ pkg/web_api/submissions.go | 7 ------- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/validator_controller/mapfixes.go b/pkg/validator_controller/mapfixes.go index bf33fcc..e273519 100644 --- a/pkg/validator_controller/mapfixes.go +++ b/pkg/validator_controller/mapfixes.go @@ -26,6 +26,8 @@ func NewMapfixesController( var( // prevent two mapfixes with same asset id ActiveMapfixStatuses = []model.MapfixStatus{ + model.MapfixStatusReleasing, + model.MapfixStatusUploaded, model.MapfixStatusUploading, model.MapfixStatusValidated, model.MapfixStatusValidating, diff --git a/pkg/validator_controller/submissions.go b/pkg/validator_controller/submissions.go index 1a373b3..44d043d 100644 --- a/pkg/validator_controller/submissions.go +++ b/pkg/validator_controller/submissions.go @@ -24,8 +24,10 @@ func NewSubmissionsController( } var( - // prevent two mapfixes with same asset id + // prevent two submissions with same asset id ActiveSubmissionStatuses = []model.SubmissionStatus{ + model.SubmissionStatusReleasing, + model.SubmissionStatusUploaded, model.SubmissionStatusUploading, model.SubmissionStatusValidated, model.SubmissionStatusValidating, diff --git a/pkg/web_api/mapfixes.go b/pkg/web_api/mapfixes.go index 048325c..f7d4cc4 100644 --- a/pkg/web_api/mapfixes.go +++ b/pkg/web_api/mapfixes.go @@ -22,6 +22,8 @@ var( } // limit mapfixes in the pipeline to one per target map ActiveAcceptedMapfixStatuses = []model.MapfixStatus{ + model.MapfixStatusReleasing, + model.MapfixStatusUploaded, model.MapfixStatusUploading, model.MapfixStatusValidated, model.MapfixStatusValidating, diff --git a/pkg/web_api/submissions.go b/pkg/web_api/submissions.go index a167454..da16abb 100644 --- a/pkg/web_api/submissions.go +++ b/pkg/web_api/submissions.go @@ -20,13 +20,6 @@ var( model.SubmissionStatusSubmitted, model.SubmissionStatusUnderConstruction, } - // limit submissions in the pipeline to one per target map - ActiveAcceptedSubmissionStatuses = []model.SubmissionStatus{ - model.SubmissionStatusUploading, - model.SubmissionStatusValidated, - model.SubmissionStatusValidating, - model.SubmissionStatusAcceptedUnvalidated, - } // Allow 5 submissions every 10 minutes CreateSubmissionRateLimit int64 = 5 CreateSubmissionRecencyWindow = time.Second*600 From 94ad0ff77449156f2e4a06aaf18a862cdc2a379e Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 12 Aug 2025 16:18:01 -0700 Subject: [PATCH 04/27] openapi: add new status endpoints --- openapi.yaml | 61 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index d233778..76c7028 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -587,7 +587,7 @@ paths: $ref: "#/components/schemas/Error" /mapfixes/{MapfixID}/status/trigger-upload: post: - summary: Role Admin changes status from Validated -> Uploading + summary: Role MapfixUpload changes status from Validated -> Uploading operationId: actionMapfixTriggerUpload tags: - Mapfixes @@ -604,7 +604,7 @@ paths: $ref: "#/components/schemas/Error" /mapfixes/{MapfixID}/status/reset-uploading: post: - summary: Role Admin manually resets uploading softlock and changes status from Uploading -> Validated + summary: Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated operationId: actionMapfixValidated tags: - Mapfixes @@ -619,6 +619,40 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /mapfixes/{MapfixID}/status/trigger-release: + post: + summary: Role MapfixUpload changes status from Uploaded -> Releasing + operationId: actionMapfixTriggerRelease + tags: + - Mapfixes + parameters: + - $ref: '#/components/parameters/MapfixID' + responses: + "204": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /mapfixes/{MapfixID}/status/reset-releasing: + post: + summary: Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded + operationId: actionMapfixUploaded + tags: + - Mapfixes + parameters: + - $ref: '#/components/parameters/MapfixID' + responses: + "204": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /operations/{OperationID}: get: summary: Retrieve operation with ID @@ -1067,7 +1101,7 @@ paths: $ref: "#/components/schemas/Error" /submissions/{SubmissionID}/status/trigger-upload: post: - summary: Role Admin changes status from Validated -> Uploading + summary: Role SubmissionUpload changes status from Validated -> Uploading operationId: actionSubmissionTriggerUpload tags: - Submissions @@ -1084,7 +1118,7 @@ paths: $ref: "#/components/schemas/Error" /submissions/{SubmissionID}/status/reset-uploading: post: - summary: Role Admin manually resets uploading softlock and changes status from Uploading -> Validated + summary: Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated operationId: actionSubmissionValidated tags: - Submissions @@ -1099,9 +1133,26 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /submissions/{SubmissionID}/status/reset-releasing: + post: + summary: Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> Uploaded + operationId: actionSubmissionUploaded + tags: + - Submissions + parameters: + - $ref: '#/components/parameters/SubmissionID' + responses: + "204": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /release-submissions: post: - summary: Release a set of uploaded maps + summary: Release a set of uploaded maps. Role SubmissionRelease operationId: releaseSubmissions tags: - Submissions From 3c9d04d6376c564499b3a354a3d8a6145e469771 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 12 Aug 2025 16:18:15 -0700 Subject: [PATCH 05/27] openapi: generate --- pkg/api/oas_client_gen.go | 414 +++++++++++++++++- pkg/api/oas_handlers_gen.go | 607 ++++++++++++++++++++++++++- pkg/api/oas_operations_gen.go | 3 + pkg/api/oas_parameters_gen.go | 249 +++++++++++ pkg/api/oas_response_decoders_gen.go | 180 ++++++++ pkg/api/oas_response_encoders_gen.go | 21 + pkg/api/oas_router_gen.go | 148 ++++++- pkg/api/oas_schemas_gen.go | 9 + pkg/api/oas_security_gen.go | 3 + pkg/api/oas_server_gen.go | 30 +- pkg/api/oas_unimplemented_gen.go | 39 +- 11 files changed, 1668 insertions(+), 35 deletions(-) diff --git a/pkg/api/oas_client_gen.go b/pkg/api/oas_client_gen.go index bc9a36b..f081c32 100644 --- a/pkg/api/oas_client_gen.go +++ b/pkg/api/oas_client_gen.go @@ -66,6 +66,12 @@ type Invoker interface { // // POST /mapfixes/{MapfixID}/status/revoke ActionMapfixRevoke(ctx context.Context, params ActionMapfixRevokeParams) error + // ActionMapfixTriggerRelease invokes actionMapfixTriggerRelease operation. + // + // Role MapfixUpload changes status from Uploaded -> Releasing. + // + // POST /mapfixes/{MapfixID}/status/trigger-release + ActionMapfixTriggerRelease(ctx context.Context, params ActionMapfixTriggerReleaseParams) error // ActionMapfixTriggerSubmit invokes actionMapfixTriggerSubmit operation. // // Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting. @@ -80,7 +86,7 @@ type Invoker interface { ActionMapfixTriggerSubmitUnchecked(ctx context.Context, params ActionMapfixTriggerSubmitUncheckedParams) error // ActionMapfixTriggerUpload invokes actionMapfixTriggerUpload operation. // - // Role Admin changes status from Validated -> Uploading. + // Role MapfixUpload changes status from Validated -> Uploading. // // POST /mapfixes/{MapfixID}/status/trigger-upload ActionMapfixTriggerUpload(ctx context.Context, params ActionMapfixTriggerUploadParams) error @@ -90,9 +96,15 @@ type Invoker interface { // // POST /mapfixes/{MapfixID}/status/trigger-validate ActionMapfixTriggerValidate(ctx context.Context, params ActionMapfixTriggerValidateParams) error + // ActionMapfixUploaded invokes actionMapfixUploaded operation. + // + // Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded. + // + // POST /mapfixes/{MapfixID}/status/reset-releasing + ActionMapfixUploaded(ctx context.Context, params ActionMapfixUploadedParams) error // ActionMapfixValidated invokes actionMapfixValidated operation. // - // Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. + // Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated. // // POST /mapfixes/{MapfixID}/status/reset-uploading ActionMapfixValidated(ctx context.Context, params ActionMapfixValidatedParams) error @@ -147,7 +159,7 @@ type Invoker interface { ActionSubmissionTriggerSubmitUnchecked(ctx context.Context, params ActionSubmissionTriggerSubmitUncheckedParams) error // ActionSubmissionTriggerUpload invokes actionSubmissionTriggerUpload operation. // - // Role Admin changes status from Validated -> Uploading. + // Role SubmissionUpload changes status from Validated -> Uploading. // // POST /submissions/{SubmissionID}/status/trigger-upload ActionSubmissionTriggerUpload(ctx context.Context, params ActionSubmissionTriggerUploadParams) error @@ -157,9 +169,17 @@ type Invoker interface { // // POST /submissions/{SubmissionID}/status/trigger-validate ActionSubmissionTriggerValidate(ctx context.Context, params ActionSubmissionTriggerValidateParams) error + // ActionSubmissionUploaded invokes actionSubmissionUploaded operation. + // + // Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> + // Uploaded. + // + // POST /submissions/{SubmissionID}/status/reset-releasing + ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error // ActionSubmissionValidated invokes actionSubmissionValidated operation. // - // Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. + // Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> + // Validated. // // POST /submissions/{SubmissionID}/status/reset-uploading ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error @@ -303,7 +323,7 @@ type Invoker interface { ListSubmissions(ctx context.Context, params ListSubmissionsParams) (*Submissions, error) // ReleaseSubmissions invokes releaseSubmissions operation. // - // Release a set of uploaded maps. + // Release a set of uploaded maps. Role SubmissionRelease. // // POST /release-submissions ReleaseSubmissions(ctx context.Context, request []ReleaseInfo) error @@ -1157,6 +1177,130 @@ func (c *Client) sendActionMapfixRevoke(ctx context.Context, params ActionMapfix return result, nil } +// ActionMapfixTriggerRelease invokes actionMapfixTriggerRelease operation. +// +// Role MapfixUpload changes status from Uploaded -> Releasing. +// +// POST /mapfixes/{MapfixID}/status/trigger-release +func (c *Client) ActionMapfixTriggerRelease(ctx context.Context, params ActionMapfixTriggerReleaseParams) error { + _, err := c.sendActionMapfixTriggerRelease(ctx, params) + return err +} + +func (c *Client) sendActionMapfixTriggerRelease(ctx context.Context, params ActionMapfixTriggerReleaseParams) (res *ActionMapfixTriggerReleaseNoContent, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionMapfixTriggerRelease"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/trigger-release"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ActionMapfixTriggerReleaseOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/mapfixes/" + { + // Encode "MapfixID" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "MapfixID", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.Int64ToString(params.MapfixID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/status/trigger-release" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:CookieAuth" + switch err := c.securityCookieAuth(ctx, ActionMapfixTriggerReleaseOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"CookieAuth\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeActionMapfixTriggerReleaseResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ActionMapfixTriggerSubmit invokes actionMapfixTriggerSubmit operation. // // Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting. @@ -1407,7 +1551,7 @@ func (c *Client) sendActionMapfixTriggerSubmitUnchecked(ctx context.Context, par // ActionMapfixTriggerUpload invokes actionMapfixTriggerUpload operation. // -// Role Admin changes status from Validated -> Uploading. +// Role MapfixUpload changes status from Validated -> Uploading. // // POST /mapfixes/{MapfixID}/status/trigger-upload func (c *Client) ActionMapfixTriggerUpload(ctx context.Context, params ActionMapfixTriggerUploadParams) error { @@ -1653,9 +1797,133 @@ func (c *Client) sendActionMapfixTriggerValidate(ctx context.Context, params Act return result, nil } +// ActionMapfixUploaded invokes actionMapfixUploaded operation. +// +// Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded. +// +// POST /mapfixes/{MapfixID}/status/reset-releasing +func (c *Client) ActionMapfixUploaded(ctx context.Context, params ActionMapfixUploadedParams) error { + _, err := c.sendActionMapfixUploaded(ctx, params) + return err +} + +func (c *Client) sendActionMapfixUploaded(ctx context.Context, params ActionMapfixUploadedParams) (res *ActionMapfixUploadedNoContent, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionMapfixUploaded"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/reset-releasing"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ActionMapfixUploadedOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/mapfixes/" + { + // Encode "MapfixID" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "MapfixID", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.Int64ToString(params.MapfixID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/status/reset-releasing" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:CookieAuth" + switch err := c.securityCookieAuth(ctx, ActionMapfixUploadedOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"CookieAuth\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeActionMapfixUploadedResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ActionMapfixValidated invokes actionMapfixValidated operation. // -// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. +// Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated. // // POST /mapfixes/{MapfixID}/status/reset-uploading func (c *Client) ActionMapfixValidated(ctx context.Context, params ActionMapfixValidatedParams) error { @@ -2772,7 +3040,7 @@ func (c *Client) sendActionSubmissionTriggerSubmitUnchecked(ctx context.Context, // ActionSubmissionTriggerUpload invokes actionSubmissionTriggerUpload operation. // -// Role Admin changes status from Validated -> Uploading. +// Role SubmissionUpload changes status from Validated -> Uploading. // // POST /submissions/{SubmissionID}/status/trigger-upload func (c *Client) ActionSubmissionTriggerUpload(ctx context.Context, params ActionSubmissionTriggerUploadParams) error { @@ -3018,9 +3286,135 @@ func (c *Client) sendActionSubmissionTriggerValidate(ctx context.Context, params return result, nil } +// ActionSubmissionUploaded invokes actionSubmissionUploaded operation. +// +// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> +// Uploaded. +// +// POST /submissions/{SubmissionID}/status/reset-releasing +func (c *Client) ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error { + _, err := c.sendActionSubmissionUploaded(ctx, params) + return err +} + +func (c *Client) sendActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) (res *ActionSubmissionUploadedNoContent, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionSubmissionUploaded"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reset-releasing"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ActionSubmissionUploadedOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/submissions/" + { + // Encode "SubmissionID" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "SubmissionID", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.Int64ToString(params.SubmissionID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/status/reset-releasing" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:CookieAuth" + switch err := c.securityCookieAuth(ctx, ActionSubmissionUploadedOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"CookieAuth\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeActionSubmissionUploadedResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ActionSubmissionValidated invokes actionSubmissionValidated operation. // -// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. +// Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> +// Validated. // // POST /submissions/{SubmissionID}/status/reset-uploading func (c *Client) ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error { @@ -6123,7 +6517,7 @@ func (c *Client) sendListSubmissions(ctx context.Context, params ListSubmissions // ReleaseSubmissions invokes releaseSubmissions operation. // -// Release a set of uploaded maps. +// Release a set of uploaded maps. Role SubmissionRelease. // // POST /release-submissions func (c *Client) ReleaseSubmissions(ctx context.Context, request []ReleaseInfo) error { diff --git a/pkg/api/oas_handlers_gen.go b/pkg/api/oas_handlers_gen.go index 197e414..9b02f6e 100644 --- a/pkg/api/oas_handlers_gen.go +++ b/pkg/api/oas_handlers_gen.go @@ -1201,6 +1201,201 @@ func (s *Server) handleActionMapfixRevokeRequest(args [1]string, argsEscaped boo } } +// handleActionMapfixTriggerReleaseRequest handles actionMapfixTriggerRelease operation. +// +// Role MapfixUpload changes status from Uploaded -> Releasing. +// +// POST /mapfixes/{MapfixID}/status/trigger-release +func (s *Server) handleActionMapfixTriggerReleaseRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionMapfixTriggerRelease"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/trigger-release"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixTriggerReleaseOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code >= 100 && code < 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ActionMapfixTriggerReleaseOperation, + ID: "actionMapfixTriggerRelease", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityCookieAuth(ctx, ActionMapfixTriggerReleaseOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "CookieAuth", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security:CookieAuth", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeActionMapfixTriggerReleaseParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *ActionMapfixTriggerReleaseNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ActionMapfixTriggerReleaseOperation, + OperationSummary: "Role MapfixUpload changes status from Uploaded -> Releasing", + OperationID: "actionMapfixTriggerRelease", + Body: nil, + Params: middleware.Parameters{ + { + Name: "MapfixID", + In: "path", + }: params.MapfixID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = ActionMapfixTriggerReleaseParams + Response = *ActionMapfixTriggerReleaseNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackActionMapfixTriggerReleaseParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.ActionMapfixTriggerRelease(ctx, params) + return response, err + }, + ) + } else { + err = s.h.ActionMapfixTriggerRelease(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeActionMapfixTriggerReleaseResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleActionMapfixTriggerSubmitRequest handles actionMapfixTriggerSubmit operation. // // Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting. @@ -1593,7 +1788,7 @@ func (s *Server) handleActionMapfixTriggerSubmitUncheckedRequest(args [1]string, // handleActionMapfixTriggerUploadRequest handles actionMapfixTriggerUpload operation. // -// Role Admin changes status from Validated -> Uploading. +// Role MapfixUpload changes status from Validated -> Uploading. // // POST /mapfixes/{MapfixID}/status/trigger-upload func (s *Server) handleActionMapfixTriggerUploadRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { @@ -1727,7 +1922,7 @@ func (s *Server) handleActionMapfixTriggerUploadRequest(args [1]string, argsEsca mreq := middleware.Request{ Context: ctx, OperationName: ActionMapfixTriggerUploadOperation, - OperationSummary: "Role Admin changes status from Validated -> Uploading", + OperationSummary: "Role MapfixUpload changes status from Validated -> Uploading", OperationID: "actionMapfixTriggerUpload", Body: nil, Params: middleware.Parameters{ @@ -1981,9 +2176,204 @@ func (s *Server) handleActionMapfixTriggerValidateRequest(args [1]string, argsEs } } +// handleActionMapfixUploadedRequest handles actionMapfixUploaded operation. +// +// Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded. +// +// POST /mapfixes/{MapfixID}/status/reset-releasing +func (s *Server) handleActionMapfixUploadedRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionMapfixUploaded"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/reset-releasing"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixUploadedOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code >= 100 && code < 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ActionMapfixUploadedOperation, + ID: "actionMapfixUploaded", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityCookieAuth(ctx, ActionMapfixUploadedOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "CookieAuth", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security:CookieAuth", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeActionMapfixUploadedParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *ActionMapfixUploadedNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ActionMapfixUploadedOperation, + OperationSummary: "Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded", + OperationID: "actionMapfixUploaded", + Body: nil, + Params: middleware.Parameters{ + { + Name: "MapfixID", + In: "path", + }: params.MapfixID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = ActionMapfixUploadedParams + Response = *ActionMapfixUploadedNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackActionMapfixUploadedParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.ActionMapfixUploaded(ctx, params) + return response, err + }, + ) + } else { + err = s.h.ActionMapfixUploaded(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeActionMapfixUploadedResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleActionMapfixValidatedRequest handles actionMapfixValidated operation. // -// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. +// Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated. // // POST /mapfixes/{MapfixID}/status/reset-uploading func (s *Server) handleActionMapfixValidatedRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { @@ -2117,7 +2507,7 @@ func (s *Server) handleActionMapfixValidatedRequest(args [1]string, argsEscaped mreq := middleware.Request{ Context: ctx, OperationName: ActionMapfixValidatedOperation, - OperationSummary: "Role Admin manually resets uploading softlock and changes status from Uploading -> Validated", + OperationSummary: "Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated", OperationID: "actionMapfixValidated", Body: nil, Params: middleware.Parameters{ @@ -3739,7 +4129,7 @@ func (s *Server) handleActionSubmissionTriggerSubmitUncheckedRequest(args [1]str // handleActionSubmissionTriggerUploadRequest handles actionSubmissionTriggerUpload operation. // -// Role Admin changes status from Validated -> Uploading. +// Role SubmissionUpload changes status from Validated -> Uploading. // // POST /submissions/{SubmissionID}/status/trigger-upload func (s *Server) handleActionSubmissionTriggerUploadRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { @@ -3873,7 +4263,7 @@ func (s *Server) handleActionSubmissionTriggerUploadRequest(args [1]string, args mreq := middleware.Request{ Context: ctx, OperationName: ActionSubmissionTriggerUploadOperation, - OperationSummary: "Role Admin changes status from Validated -> Uploading", + OperationSummary: "Role SubmissionUpload changes status from Validated -> Uploading", OperationID: "actionSubmissionTriggerUpload", Body: nil, Params: middleware.Parameters{ @@ -4127,9 +4517,206 @@ func (s *Server) handleActionSubmissionTriggerValidateRequest(args [1]string, ar } } +// handleActionSubmissionUploadedRequest handles actionSubmissionUploaded operation. +// +// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> +// Uploaded. +// +// POST /submissions/{SubmissionID}/status/reset-releasing +func (s *Server) handleActionSubmissionUploadedRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("actionSubmissionUploaded"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reset-releasing"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionUploadedOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code >= 100 && code < 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ActionSubmissionUploadedOperation, + ID: "actionSubmissionUploaded", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityCookieAuth(ctx, ActionSubmissionUploadedOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "CookieAuth", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security:CookieAuth", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeActionSubmissionUploadedParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *ActionSubmissionUploadedNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ActionSubmissionUploadedOperation, + OperationSummary: "Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> Uploaded", + OperationID: "actionSubmissionUploaded", + Body: nil, + Params: middleware.Parameters{ + { + Name: "SubmissionID", + In: "path", + }: params.SubmissionID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = ActionSubmissionUploadedParams + Response = *ActionSubmissionUploadedNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackActionSubmissionUploadedParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.ActionSubmissionUploaded(ctx, params) + return response, err + }, + ) + } else { + err = s.h.ActionSubmissionUploaded(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeActionSubmissionUploadedResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleActionSubmissionValidatedRequest handles actionSubmissionValidated operation. // -// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. +// Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> +// Validated. // // POST /submissions/{SubmissionID}/status/reset-uploading func (s *Server) handleActionSubmissionValidatedRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { @@ -4263,7 +4850,7 @@ func (s *Server) handleActionSubmissionValidatedRequest(args [1]string, argsEsca mreq := middleware.Request{ Context: ctx, OperationName: ActionSubmissionValidatedOperation, - OperationSummary: "Role Admin manually resets uploading softlock and changes status from Uploading -> Validated", + OperationSummary: "Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated", OperationID: "actionSubmissionValidated", Body: nil, Params: middleware.Parameters{ @@ -8435,7 +9022,7 @@ func (s *Server) handleListSubmissionsRequest(args [0]string, argsEscaped bool, // handleReleaseSubmissionsRequest handles releaseSubmissions operation. // -// Release a set of uploaded maps. +// Release a set of uploaded maps. Role SubmissionRelease. // // POST /release-submissions func (s *Server) handleReleaseSubmissionsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { @@ -8574,7 +9161,7 @@ func (s *Server) handleReleaseSubmissionsRequest(args [0]string, argsEscaped boo mreq := middleware.Request{ Context: ctx, OperationName: ReleaseSubmissionsOperation, - OperationSummary: "Release a set of uploaded maps", + OperationSummary: "Release a set of uploaded maps. Role SubmissionRelease", OperationID: "releaseSubmissions", Body: request, Params: middleware.Parameters{}, diff --git a/pkg/api/oas_operations_gen.go b/pkg/api/oas_operations_gen.go index b5a33a8..ec1d73d 100644 --- a/pkg/api/oas_operations_gen.go +++ b/pkg/api/oas_operations_gen.go @@ -12,10 +12,12 @@ const ( ActionMapfixResetSubmittingOperation OperationName = "ActionMapfixResetSubmitting" ActionMapfixRetryValidateOperation OperationName = "ActionMapfixRetryValidate" ActionMapfixRevokeOperation OperationName = "ActionMapfixRevoke" + ActionMapfixTriggerReleaseOperation OperationName = "ActionMapfixTriggerRelease" ActionMapfixTriggerSubmitOperation OperationName = "ActionMapfixTriggerSubmit" ActionMapfixTriggerSubmitUncheckedOperation OperationName = "ActionMapfixTriggerSubmitUnchecked" ActionMapfixTriggerUploadOperation OperationName = "ActionMapfixTriggerUpload" ActionMapfixTriggerValidateOperation OperationName = "ActionMapfixTriggerValidate" + ActionMapfixUploadedOperation OperationName = "ActionMapfixUploaded" ActionMapfixValidatedOperation OperationName = "ActionMapfixValidated" ActionSubmissionAcceptedOperation OperationName = "ActionSubmissionAccepted" ActionSubmissionRejectOperation OperationName = "ActionSubmissionReject" @@ -27,6 +29,7 @@ const ( ActionSubmissionTriggerSubmitUncheckedOperation OperationName = "ActionSubmissionTriggerSubmitUnchecked" ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload" ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate" + ActionSubmissionUploadedOperation OperationName = "ActionSubmissionUploaded" ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated" CreateMapfixOperation OperationName = "CreateMapfix" CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment" diff --git a/pkg/api/oas_parameters_gen.go b/pkg/api/oas_parameters_gen.go index ac8c0f6..ba64fd2 100644 --- a/pkg/api/oas_parameters_gen.go +++ b/pkg/api/oas_parameters_gen.go @@ -513,6 +513,89 @@ func decodeActionMapfixRevokeParams(args [1]string, argsEscaped bool, r *http.Re return params, nil } +// ActionMapfixTriggerReleaseParams is parameters of actionMapfixTriggerRelease operation. +type ActionMapfixTriggerReleaseParams struct { + // The unique identifier for a mapfix. + MapfixID int64 +} + +func unpackActionMapfixTriggerReleaseParams(packed middleware.Parameters) (params ActionMapfixTriggerReleaseParams) { + { + key := middleware.ParameterKey{ + Name: "MapfixID", + In: "path", + } + params.MapfixID = packed[key].(int64) + } + return params +} + +func decodeActionMapfixTriggerReleaseParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionMapfixTriggerReleaseParams, _ error) { + // Decode path: MapfixID. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "MapfixID", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.MapfixID = c + return nil + }(); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.MapfixID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "MapfixID", + In: "path", + Err: err, + } + } + return params, nil +} + // ActionMapfixTriggerSubmitParams is parameters of actionMapfixTriggerSubmit operation. type ActionMapfixTriggerSubmitParams struct { // The unique identifier for a mapfix. @@ -845,6 +928,89 @@ func decodeActionMapfixTriggerValidateParams(args [1]string, argsEscaped bool, r return params, nil } +// ActionMapfixUploadedParams is parameters of actionMapfixUploaded operation. +type ActionMapfixUploadedParams struct { + // The unique identifier for a mapfix. + MapfixID int64 +} + +func unpackActionMapfixUploadedParams(packed middleware.Parameters) (params ActionMapfixUploadedParams) { + { + key := middleware.ParameterKey{ + Name: "MapfixID", + In: "path", + } + params.MapfixID = packed[key].(int64) + } + return params +} + +func decodeActionMapfixUploadedParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionMapfixUploadedParams, _ error) { + // Decode path: MapfixID. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "MapfixID", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.MapfixID = c + return nil + }(); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.MapfixID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "MapfixID", + In: "path", + Err: err, + } + } + return params, nil +} + // ActionMapfixValidatedParams is parameters of actionMapfixValidated operation. type ActionMapfixValidatedParams struct { // The unique identifier for a mapfix. @@ -1758,6 +1924,89 @@ func decodeActionSubmissionTriggerValidateParams(args [1]string, argsEscaped boo return params, nil } +// ActionSubmissionUploadedParams is parameters of actionSubmissionUploaded operation. +type ActionSubmissionUploadedParams struct { + // The unique identifier for a submission. + SubmissionID int64 +} + +func unpackActionSubmissionUploadedParams(packed middleware.Parameters) (params ActionSubmissionUploadedParams) { + { + key := middleware.ParameterKey{ + Name: "SubmissionID", + In: "path", + } + params.SubmissionID = packed[key].(int64) + } + return params +} + +func decodeActionSubmissionUploadedParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionSubmissionUploadedParams, _ error) { + // Decode path: SubmissionID. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "SubmissionID", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.SubmissionID = c + return nil + }(); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.SubmissionID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "SubmissionID", + In: "path", + Err: err, + } + } + return params, nil +} + // ActionSubmissionValidatedParams is parameters of actionSubmissionValidated operation. type ActionSubmissionValidatedParams struct { // The unique identifier for a submission. diff --git a/pkg/api/oas_response_decoders_gen.go b/pkg/api/oas_response_decoders_gen.go index 958d0d9..325fdeb 100644 --- a/pkg/api/oas_response_decoders_gen.go +++ b/pkg/api/oas_response_decoders_gen.go @@ -376,6 +376,66 @@ func decodeActionMapfixRevokeResponse(resp *http.Response) (res *ActionMapfixRev return res, errors.Wrap(defRes, "error") } +func decodeActionMapfixTriggerReleaseResponse(resp *http.Response) (res *ActionMapfixTriggerReleaseNoContent, _ error) { + switch resp.StatusCode { + case 204: + // Code 204. + return &ActionMapfixTriggerReleaseNoContent{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeActionMapfixTriggerSubmitResponse(resp *http.Response) (res *ActionMapfixTriggerSubmitNoContent, _ error) { switch resp.StatusCode { case 204: @@ -616,6 +676,66 @@ func decodeActionMapfixTriggerValidateResponse(resp *http.Response) (res *Action return res, errors.Wrap(defRes, "error") } +func decodeActionMapfixUploadedResponse(resp *http.Response) (res *ActionMapfixUploadedNoContent, _ error) { + switch resp.StatusCode { + case 204: + // Code 204. + return &ActionMapfixUploadedNoContent{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeActionMapfixValidatedResponse(resp *http.Response) (res *ActionMapfixValidatedNoContent, _ error) { switch resp.StatusCode { case 204: @@ -1276,6 +1396,66 @@ func decodeActionSubmissionTriggerValidateResponse(resp *http.Response) (res *Ac return res, errors.Wrap(defRes, "error") } +func decodeActionSubmissionUploadedResponse(resp *http.Response) (res *ActionSubmissionUploadedNoContent, _ error) { + switch resp.StatusCode { + case 204: + // Code 204. + return &ActionSubmissionUploadedNoContent{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeActionSubmissionValidatedResponse(resp *http.Response) (res *ActionSubmissionValidatedNoContent, _ error) { switch resp.StatusCode { case 204: diff --git a/pkg/api/oas_response_encoders_gen.go b/pkg/api/oas_response_encoders_gen.go index fb923c6..14300be 100644 --- a/pkg/api/oas_response_encoders_gen.go +++ b/pkg/api/oas_response_encoders_gen.go @@ -56,6 +56,13 @@ func encodeActionMapfixRevokeResponse(response *ActionMapfixRevokeNoContent, w h return nil } +func encodeActionMapfixTriggerReleaseResponse(response *ActionMapfixTriggerReleaseNoContent, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(204) + span.SetStatus(codes.Ok, http.StatusText(204)) + + return nil +} + func encodeActionMapfixTriggerSubmitResponse(response *ActionMapfixTriggerSubmitNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) @@ -84,6 +91,13 @@ func encodeActionMapfixTriggerValidateResponse(response *ActionMapfixTriggerVali return nil } +func encodeActionMapfixUploadedResponse(response *ActionMapfixUploadedNoContent, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(204) + span.SetStatus(codes.Ok, http.StatusText(204)) + + return nil +} + func encodeActionMapfixValidatedResponse(response *ActionMapfixValidatedNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) @@ -161,6 +175,13 @@ func encodeActionSubmissionTriggerValidateResponse(response *ActionSubmissionTri return nil } +func encodeActionSubmissionUploadedResponse(response *ActionSubmissionUploadedNoContent, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(204) + span.SetStatus(codes.Ok, http.StatusText(204)) + + return nil +} + func encodeActionSubmissionValidatedResponse(response *ActionSubmissionValidatedNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) diff --git a/pkg/api/oas_router_gen.go b/pkg/api/oas_router_gen.go index 1dc226d..2ea8902 100644 --- a/pkg/api/oas_router_gen.go +++ b/pkg/api/oas_router_gen.go @@ -318,6 +318,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { + case 'r': // Prefix: "releasing" + + if l := len("releasing"); len(elem) >= l && elem[0:l] == "releasing" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleActionMapfixUploadedRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + case 's': // Prefix: "submitting" if l := len("submitting"); len(elem) >= l && elem[0:l] == "submitting" { @@ -444,6 +466,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { + case 'r': // Prefix: "release" + + if l := len("release"); len(elem) >= l && elem[0:l] == "release" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleActionMapfixTriggerReleaseRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + case 's': // Prefix: "submit" if l := len("submit"); len(elem) >= l && elem[0:l] == "submit" { @@ -1160,6 +1204,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { + case 'r': // Prefix: "releasing" + + if l := len("releasing"); len(elem) >= l && elem[0:l] == "releasing" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleActionSubmissionUploadedRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + case 's': // Prefix: "submitting" if l := len("submitting"); len(elem) >= l && elem[0:l] == "submitting" { @@ -1762,6 +1828,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { + case 'r': // Prefix: "releasing" + + if l := len("releasing"); len(elem) >= l && elem[0:l] == "releasing" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ActionMapfixUploadedOperation + r.summary = "Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded" + r.operationID = "actionMapfixUploaded" + r.pathPattern = "/mapfixes/{MapfixID}/status/reset-releasing" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + case 's': // Prefix: "submitting" if l := len("submitting"); len(elem) >= l && elem[0:l] == "submitting" { @@ -1799,7 +1889,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { switch method { case "POST": r.name = ActionMapfixValidatedOperation - r.summary = "Role Admin manually resets uploading softlock and changes status from Uploading -> Validated" + r.summary = "Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated" r.operationID = "actionMapfixValidated" r.pathPattern = "/mapfixes/{MapfixID}/status/reset-uploading" r.args = args @@ -1898,6 +1988,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { + case 'r': // Prefix: "release" + + if l := len("release"); len(elem) >= l && elem[0:l] == "release" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ActionMapfixTriggerReleaseOperation + r.summary = "Role MapfixUpload changes status from Uploaded -> Releasing" + r.operationID = "actionMapfixTriggerRelease" + r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-release" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + case 's': // Prefix: "submit" if l := len("submit"); len(elem) >= l && elem[0:l] == "submit" { @@ -1960,7 +2074,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { switch method { case "POST": r.name = ActionMapfixTriggerUploadOperation - r.summary = "Role Admin changes status from Validated -> Uploading" + r.summary = "Role MapfixUpload changes status from Validated -> Uploading" r.operationID = "actionMapfixTriggerUpload" r.pathPattern = "/mapfixes/{MapfixID}/status/trigger-upload" r.args = args @@ -2136,7 +2250,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { switch method { case "POST": r.name = ReleaseSubmissionsOperation - r.summary = "Release a set of uploaded maps" + r.summary = "Release a set of uploaded maps. Role SubmissionRelease" r.operationID = "releaseSubmissions" r.pathPattern = "/release-submissions" r.args = args @@ -2716,6 +2830,30 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { + case 'r': // Prefix: "releasing" + + if l := len("releasing"); len(elem) >= l && elem[0:l] == "releasing" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ActionSubmissionUploadedOperation + r.summary = "Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> Uploaded" + r.operationID = "actionSubmissionUploaded" + r.pathPattern = "/submissions/{SubmissionID}/status/reset-releasing" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + case 's': // Prefix: "submitting" if l := len("submitting"); len(elem) >= l && elem[0:l] == "submitting" { @@ -2753,7 +2891,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { switch method { case "POST": r.name = ActionSubmissionValidatedOperation - r.summary = "Role Admin manually resets uploading softlock and changes status from Uploading -> Validated" + r.summary = "Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated" r.operationID = "actionSubmissionValidated" r.pathPattern = "/submissions/{SubmissionID}/status/reset-uploading" r.args = args @@ -2914,7 +3052,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { switch method { case "POST": r.name = ActionSubmissionTriggerUploadOperation - r.summary = "Role Admin changes status from Validated -> Uploading" + r.summary = "Role SubmissionUpload changes status from Validated -> Uploading" r.operationID = "actionSubmissionTriggerUpload" r.pathPattern = "/submissions/{SubmissionID}/status/trigger-upload" r.args = args diff --git a/pkg/api/oas_schemas_gen.go b/pkg/api/oas_schemas_gen.go index b65b80d..91c3ccb 100644 --- a/pkg/api/oas_schemas_gen.go +++ b/pkg/api/oas_schemas_gen.go @@ -32,6 +32,9 @@ type ActionMapfixRetryValidateNoContent struct{} // ActionMapfixRevokeNoContent is response for ActionMapfixRevoke operation. type ActionMapfixRevokeNoContent struct{} +// ActionMapfixTriggerReleaseNoContent is response for ActionMapfixTriggerRelease operation. +type ActionMapfixTriggerReleaseNoContent struct{} + // ActionMapfixTriggerSubmitNoContent is response for ActionMapfixTriggerSubmit operation. type ActionMapfixTriggerSubmitNoContent struct{} @@ -44,6 +47,9 @@ type ActionMapfixTriggerUploadNoContent struct{} // ActionMapfixTriggerValidateNoContent is response for ActionMapfixTriggerValidate operation. type ActionMapfixTriggerValidateNoContent struct{} +// ActionMapfixUploadedNoContent is response for ActionMapfixUploaded operation. +type ActionMapfixUploadedNoContent struct{} + // ActionMapfixValidatedNoContent is response for ActionMapfixValidated operation. type ActionMapfixValidatedNoContent struct{} @@ -77,6 +83,9 @@ type ActionSubmissionTriggerUploadNoContent struct{} // ActionSubmissionTriggerValidateNoContent is response for ActionSubmissionTriggerValidate operation. type ActionSubmissionTriggerValidateNoContent struct{} +// ActionSubmissionUploadedNoContent is response for ActionSubmissionUploaded operation. +type ActionSubmissionUploadedNoContent struct{} + // ActionSubmissionValidatedNoContent is response for ActionSubmissionValidated operation. type ActionSubmissionValidatedNoContent struct{} diff --git a/pkg/api/oas_security_gen.go b/pkg/api/oas_security_gen.go index 100c173..96068eb 100644 --- a/pkg/api/oas_security_gen.go +++ b/pkg/api/oas_security_gen.go @@ -40,10 +40,12 @@ var operationRolesCookieAuth = map[string][]string{ ActionMapfixResetSubmittingOperation: []string{}, ActionMapfixRetryValidateOperation: []string{}, ActionMapfixRevokeOperation: []string{}, + ActionMapfixTriggerReleaseOperation: []string{}, ActionMapfixTriggerSubmitOperation: []string{}, ActionMapfixTriggerSubmitUncheckedOperation: []string{}, ActionMapfixTriggerUploadOperation: []string{}, ActionMapfixTriggerValidateOperation: []string{}, + ActionMapfixUploadedOperation: []string{}, ActionMapfixValidatedOperation: []string{}, ActionSubmissionAcceptedOperation: []string{}, ActionSubmissionRejectOperation: []string{}, @@ -55,6 +57,7 @@ var operationRolesCookieAuth = map[string][]string{ ActionSubmissionTriggerSubmitUncheckedOperation: []string{}, ActionSubmissionTriggerUploadOperation: []string{}, ActionSubmissionTriggerValidateOperation: []string{}, + ActionSubmissionUploadedOperation: []string{}, ActionSubmissionValidatedOperation: []string{}, CreateMapfixOperation: []string{}, CreateMapfixAuditCommentOperation: []string{}, diff --git a/pkg/api/oas_server_gen.go b/pkg/api/oas_server_gen.go index bc8d72e..edde10a 100644 --- a/pkg/api/oas_server_gen.go +++ b/pkg/api/oas_server_gen.go @@ -45,6 +45,12 @@ type Handler interface { // // POST /mapfixes/{MapfixID}/status/revoke ActionMapfixRevoke(ctx context.Context, params ActionMapfixRevokeParams) error + // ActionMapfixTriggerRelease implements actionMapfixTriggerRelease operation. + // + // Role MapfixUpload changes status from Uploaded -> Releasing. + // + // POST /mapfixes/{MapfixID}/status/trigger-release + ActionMapfixTriggerRelease(ctx context.Context, params ActionMapfixTriggerReleaseParams) error // ActionMapfixTriggerSubmit implements actionMapfixTriggerSubmit operation. // // Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting. @@ -59,7 +65,7 @@ type Handler interface { ActionMapfixTriggerSubmitUnchecked(ctx context.Context, params ActionMapfixTriggerSubmitUncheckedParams) error // ActionMapfixTriggerUpload implements actionMapfixTriggerUpload operation. // - // Role Admin changes status from Validated -> Uploading. + // Role MapfixUpload changes status from Validated -> Uploading. // // POST /mapfixes/{MapfixID}/status/trigger-upload ActionMapfixTriggerUpload(ctx context.Context, params ActionMapfixTriggerUploadParams) error @@ -69,9 +75,15 @@ type Handler interface { // // POST /mapfixes/{MapfixID}/status/trigger-validate ActionMapfixTriggerValidate(ctx context.Context, params ActionMapfixTriggerValidateParams) error + // ActionMapfixUploaded implements actionMapfixUploaded operation. + // + // Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded. + // + // POST /mapfixes/{MapfixID}/status/reset-releasing + ActionMapfixUploaded(ctx context.Context, params ActionMapfixUploadedParams) error // ActionMapfixValidated implements actionMapfixValidated operation. // - // Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. + // Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated. // // POST /mapfixes/{MapfixID}/status/reset-uploading ActionMapfixValidated(ctx context.Context, params ActionMapfixValidatedParams) error @@ -126,7 +138,7 @@ type Handler interface { ActionSubmissionTriggerSubmitUnchecked(ctx context.Context, params ActionSubmissionTriggerSubmitUncheckedParams) error // ActionSubmissionTriggerUpload implements actionSubmissionTriggerUpload operation. // - // Role Admin changes status from Validated -> Uploading. + // Role SubmissionUpload changes status from Validated -> Uploading. // // POST /submissions/{SubmissionID}/status/trigger-upload ActionSubmissionTriggerUpload(ctx context.Context, params ActionSubmissionTriggerUploadParams) error @@ -136,9 +148,17 @@ type Handler interface { // // POST /submissions/{SubmissionID}/status/trigger-validate ActionSubmissionTriggerValidate(ctx context.Context, params ActionSubmissionTriggerValidateParams) error + // ActionSubmissionUploaded implements actionSubmissionUploaded operation. + // + // Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> + // Uploaded. + // + // POST /submissions/{SubmissionID}/status/reset-releasing + ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error // ActionSubmissionValidated implements actionSubmissionValidated operation. // - // Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. + // Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> + // Validated. // // POST /submissions/{SubmissionID}/status/reset-uploading ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error @@ -282,7 +302,7 @@ type Handler interface { ListSubmissions(ctx context.Context, params ListSubmissionsParams) (*Submissions, error) // ReleaseSubmissions implements releaseSubmissions operation. // - // Release a set of uploaded maps. + // Release a set of uploaded maps. Role SubmissionRelease. // // POST /release-submissions ReleaseSubmissions(ctx context.Context, req []ReleaseInfo) error diff --git a/pkg/api/oas_unimplemented_gen.go b/pkg/api/oas_unimplemented_gen.go index 2c74330..6f9106c 100644 --- a/pkg/api/oas_unimplemented_gen.go +++ b/pkg/api/oas_unimplemented_gen.go @@ -68,6 +68,15 @@ func (UnimplementedHandler) ActionMapfixRevoke(ctx context.Context, params Actio return ht.ErrNotImplemented } +// ActionMapfixTriggerRelease implements actionMapfixTriggerRelease operation. +// +// Role MapfixUpload changes status from Uploaded -> Releasing. +// +// POST /mapfixes/{MapfixID}/status/trigger-release +func (UnimplementedHandler) ActionMapfixTriggerRelease(ctx context.Context, params ActionMapfixTriggerReleaseParams) error { + return ht.ErrNotImplemented +} + // ActionMapfixTriggerSubmit implements actionMapfixTriggerSubmit operation. // // Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting. @@ -88,7 +97,7 @@ func (UnimplementedHandler) ActionMapfixTriggerSubmitUnchecked(ctx context.Conte // ActionMapfixTriggerUpload implements actionMapfixTriggerUpload operation. // -// Role Admin changes status from Validated -> Uploading. +// Role MapfixUpload changes status from Validated -> Uploading. // // POST /mapfixes/{MapfixID}/status/trigger-upload func (UnimplementedHandler) ActionMapfixTriggerUpload(ctx context.Context, params ActionMapfixTriggerUploadParams) error { @@ -104,9 +113,18 @@ func (UnimplementedHandler) ActionMapfixTriggerValidate(ctx context.Context, par return ht.ErrNotImplemented } +// ActionMapfixUploaded implements actionMapfixUploaded operation. +// +// Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded. +// +// POST /mapfixes/{MapfixID}/status/reset-releasing +func (UnimplementedHandler) ActionMapfixUploaded(ctx context.Context, params ActionMapfixUploadedParams) error { + return ht.ErrNotImplemented +} + // ActionMapfixValidated implements actionMapfixValidated operation. // -// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. +// Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated. // // POST /mapfixes/{MapfixID}/status/reset-uploading func (UnimplementedHandler) ActionMapfixValidated(ctx context.Context, params ActionMapfixValidatedParams) error { @@ -188,7 +206,7 @@ func (UnimplementedHandler) ActionSubmissionTriggerSubmitUnchecked(ctx context.C // ActionSubmissionTriggerUpload implements actionSubmissionTriggerUpload operation. // -// Role Admin changes status from Validated -> Uploading. +// Role SubmissionUpload changes status from Validated -> Uploading. // // POST /submissions/{SubmissionID}/status/trigger-upload func (UnimplementedHandler) ActionSubmissionTriggerUpload(ctx context.Context, params ActionSubmissionTriggerUploadParams) error { @@ -204,9 +222,20 @@ func (UnimplementedHandler) ActionSubmissionTriggerValidate(ctx context.Context, return ht.ErrNotImplemented } +// ActionSubmissionUploaded implements actionSubmissionUploaded operation. +// +// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> +// Uploaded. +// +// POST /submissions/{SubmissionID}/status/reset-releasing +func (UnimplementedHandler) ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error { + return ht.ErrNotImplemented +} + // ActionSubmissionValidated implements actionSubmissionValidated operation. // -// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated. +// Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> +// Validated. // // POST /submissions/{SubmissionID}/status/reset-uploading func (UnimplementedHandler) ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error { @@ -422,7 +451,7 @@ func (UnimplementedHandler) ListSubmissions(ctx context.Context, params ListSubm // ReleaseSubmissions implements releaseSubmissions operation. // -// Release a set of uploaded maps. +// Release a set of uploaded maps. Role SubmissionRelease. // // POST /release-submissions func (UnimplementedHandler) ReleaseSubmissions(ctx context.Context, req []ReleaseInfo) error { From 807d394646c0918062934de0b2886b510fd6d8e4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 12 Aug 2025 17:46:33 -0700 Subject: [PATCH 06/27] web: add release buttons --- .../app/_components/review/ReviewButtons.tsx | 21 +++++++++++++++++++ web/src/app/ts/Mapfix.ts | 9 ++++++-- web/src/app/ts/Roles.ts | 1 + web/src/app/ts/Status.ts | 5 +++-- web/src/app/ts/Submission.ts | 3 +++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/web/src/app/_components/review/ReviewButtons.tsx b/web/src/app/_components/review/ReviewButtons.tsx index a35282d..96fae68 100644 --- a/web/src/app/_components/review/ReviewButtons.tsx +++ b/web/src/app/_components/review/ReviewButtons.tsx @@ -30,6 +30,8 @@ const ReviewActions = { RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction, Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction, ResetUploading: {name:"Reset Uploading",action:"reset-uploading"} as ReviewAction, + Release: {name:"Release",action:"trigger-release"} as ReviewAction, + ResetReleasing: {name:"Reset Releasing",action:"reset-releasing"} as ReviewAction, } const ReviewButtons: React.FC = ({ @@ -54,6 +56,7 @@ const ReviewButtons: React.FC = ({ const reviewRole = type === "submission" ? RolesConstants.SubmissionReview : RolesConstants.MapfixReview; const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload; + const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease; if (is_submitter) { if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) { @@ -139,6 +142,24 @@ const ReviewButtons: React.FC = ({ } } + // Buttons for release role + if (hasRole(roles, releaseRole)) { + // submissions do not have a release button + if (type === "mapfix" && status === Status.Uploaded) { + buttons.push({ + action: ReviewActions.Release, + color: "success" + }); + } + + if (status === Status.Releasing) { + buttons.push({ + action: ReviewActions.ResetReleasing, + color: "warning" + }); + } + } + return buttons; }; diff --git a/web/src/app/ts/Mapfix.ts b/web/src/app/ts/Mapfix.ts index a09ed2b..2fabdb1 100644 --- a/web/src/app/ts/Mapfix.ts +++ b/web/src/app/ts/Mapfix.ts @@ -8,8 +8,9 @@ const enum MapfixStatus { Validated = 6, Uploading = 7, Uploaded = 8, + Releasing = 11, Rejected = 9, - // MapfixStatus does not have a Released state + Released = 10, } interface MapfixInfo { @@ -36,8 +37,12 @@ interface MapfixList { function MapfixStatusToString(mapfix_status: MapfixStatus): string { switch (mapfix_status) { + case MapfixStatus.Released: + return "RELEASED" case MapfixStatus.Rejected: return "REJECTED" + case MapfixStatus.Releasing: + return "RELEASING" case MapfixStatus.Uploading: return "UPLOADING" case MapfixStatus.Uploaded: @@ -47,7 +52,7 @@ function MapfixStatusToString(mapfix_status: MapfixStatus): string { case MapfixStatus.Validating: return "VALIDATING" case MapfixStatus.AcceptedUnvalidated: - return "ACCEPTED, NOT VALIDATED" + return "SCRIPT REVIEW" case MapfixStatus.ChangesRequested: return "CHANGES REQUESTED" case MapfixStatus.Submitted: diff --git a/web/src/app/ts/Roles.ts b/web/src/app/ts/Roles.ts index 57a7d9d..626dee2 100644 --- a/web/src/app/ts/Roles.ts +++ b/web/src/app/ts/Roles.ts @@ -9,6 +9,7 @@ const RolesConstants = { ScriptWrite: 1 << 3 as Roles, MapfixUpload: 1 << 2 as Roles, MapfixReview: 1 << 1 as Roles, + MapfixRelease: 1 << 2 as Roles, // same as upload MapDownload: 1 << 0 as Roles, Empty: 0 as Roles, }; diff --git a/web/src/app/ts/Status.ts b/web/src/app/ts/Status.ts index 869b72a..83c3a4d 100644 --- a/web/src/app/ts/Status.ts +++ b/web/src/app/ts/Status.ts @@ -11,9 +11,10 @@ export const Status = { Uploading: SubmissionStatus.Uploading, Uploaded: SubmissionStatus.Uploaded, Rejected: SubmissionStatus.Rejected, - Release: SubmissionStatus.Released + Release: SubmissionStatus.Released, + Releasing: SubmissionStatus.Releasing, }; export const StatusMatches = (status: number, statusValues: number[]) => { return statusValues.includes(status); -}; \ No newline at end of file +}; diff --git a/web/src/app/ts/Submission.ts b/web/src/app/ts/Submission.ts index 4d7b18d..2e0f6ee 100644 --- a/web/src/app/ts/Submission.ts +++ b/web/src/app/ts/Submission.ts @@ -8,6 +8,7 @@ const enum SubmissionStatus { Validated = 6, Uploading = 7, Uploaded = 8, + Releasing = 11, Rejected = 9, Released = 10, } @@ -39,6 +40,8 @@ function SubmissionStatusToString(submission_status: SubmissionStatus): string { return "RELEASED" case SubmissionStatus.Rejected: return "REJECTED" + case SubmissionStatus.Releasing: + return "RELEASING" case SubmissionStatus.Uploading: return "UPLOADING" case SubmissionStatus.Uploaded: From 0cb419430ae1b3ba7912291220f6cd382415b77f Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 12 Aug 2025 16:57:23 -0700 Subject: [PATCH 07/27] backend: make release pipeline internals --- go.mod | 2 +- go.sum | 4 +- pkg/model/nats.go | 26 +++++ pkg/service/nats_mapfix.go | 26 +++++ pkg/service/nats_submission.go | 22 +++++ pkg/validator_controller/mapfixes.go | 113 ++++++++++++++++++++- pkg/validator_controller/submissions.go | 98 ++++++++++++++++++- pkg/web_api/mapfixes.go | 121 +++++++++++++++++++++++ pkg/web_api/submissions.go | 125 +++++++++++++++++++----- 9 files changed, 507 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 4e1235c..9a20192 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.5 require ( git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed - git.itzana.me/strafesnet/go-grpc v0.0.0-20250807005013-301d35b914ef + git.itzana.me/strafesnet/go-grpc v0.0.0-20250814235656-6a41b31a1a33 git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9 github.com/dchest/siphash v1.2.3 github.com/gin-gonic/gin v1.10.1 diff --git a/go.sum b/go.sum index 3e3ccdf..8a95f7f 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed h1:eGWIQx2AOrSsLC2dieuSs8MCliRE60tvpZnmxsTBtKc= git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed/go.mod h1:KJal0K++M6HEzSry6JJ2iDPZtOQn5zSstNlDbU3X4Jg= -git.itzana.me/strafesnet/go-grpc v0.0.0-20250807005013-301d35b914ef h1:SJi4V4+xzScFnbMRN1gkZxcqR1xKfiT7CaXanLltEzw= -git.itzana.me/strafesnet/go-grpc v0.0.0-20250807005013-301d35b914ef/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs= +git.itzana.me/strafesnet/go-grpc v0.0.0-20250814235656-6a41b31a1a33 h1:7dzfLFXcrNHRSrS1CxTDC9yQLIfbcsQHLFIPeTyMFbE= +git.itzana.me/strafesnet/go-grpc v0.0.0-20250814235656-6a41b31a1a33/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs= git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9 h1:7lU6jyR7S7Rhh1dnUp7GyIRHUTBXZagw8F4n4hOyxLw= git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9/go.mod h1:uyYerSieEt4v0MJCdPLppG0LtJ4Yj035vuTetWGsxjY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/pkg/model/nats.go b/pkg/model/nats.go index 9a1a8fd..081ba13 100644 --- a/pkg/model/nats.go +++ b/pkg/model/nats.go @@ -65,3 +65,29 @@ type UploadMapfixRequest struct { ModelVersion uint64 TargetAssetID uint64 } + +type ReleaseSubmissionRequest struct { + // Release schedule + SubmissionID int64 + ReleaseDate int64 + // Model download info + ModelID uint64 + ModelVersion uint64 + // MapCreate + UploadedAssetID uint64 + DisplayName string + Creator string + GameID uint32 + Submitter uint64 +} +type BatchReleaseSubmissionsRequest struct { + Submissions []ReleaseSubmissionRequest + OperationID int32 +} + +type ReleaseMapfixRequest struct { + MapfixID int64 + ModelID uint64 + ModelVersion uint64 + TargetAssetID uint64 +} diff --git a/pkg/service/nats_mapfix.go b/pkg/service/nats_mapfix.go index 6bf33a7..62989c2 100644 --- a/pkg/service/nats_mapfix.go +++ b/pkg/service/nats_mapfix.go @@ -112,3 +112,29 @@ func (svc *Service) NatsValidateMapfix( return nil } + +func (svc *Service) NatsReleaseMapfix( + MapfixID int64, + ModelID uint64, + ModelVersion uint64, + TargetAssetID uint64, +) error { + release_fix_request := model.ReleaseMapfixRequest{ + MapfixID: MapfixID, + ModelID: ModelID, + ModelVersion: ModelVersion, + TargetAssetID: TargetAssetID, + } + + j, err := json.Marshal(release_fix_request) + if err != nil { + return err + } + + _, err = svc.nats.Publish("maptest.mapfixes.release", []byte(j)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/service/nats_submission.go b/pkg/service/nats_submission.go index fdfa388..a48c4fd 100644 --- a/pkg/service/nats_submission.go +++ b/pkg/service/nats_submission.go @@ -88,6 +88,28 @@ func (svc *Service) NatsUploadSubmission( return nil } +func (svc *Service) NatsBatchReleaseSubmissions( + Submissions []model.ReleaseSubmissionRequest, + operation int32, +) error { + release_new_request := model.BatchReleaseSubmissionsRequest{ + Submissions: Submissions, + OperationID: operation, + } + + j, err := json.Marshal(release_new_request) + if err != nil { + return err + } + + _, err = svc.nats.Publish("maptest.submissions.batchrelease", []byte(j)) + if err != nil { + return err + } + + return nil +} + func (svc *Service) NatsValidateSubmission( SubmissionID int64, ModelID uint64, diff --git a/pkg/validator_controller/mapfixes.go b/pkg/validator_controller/mapfixes.go index e273519..eb5d8ea 100644 --- a/pkg/validator_controller/mapfixes.go +++ b/pkg/validator_controller/mapfixes.go @@ -186,7 +186,7 @@ func (svc *Mapfixes) SetStatusValidated(ctx context.Context, params *validator.M // (Internal endpoint) Role Validator changes status from Validating -> Accepted. // // POST /mapfixes/{MapfixID}/status/validator-failed -func (svc *Mapfixes) SetStatusFailed(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) { +func (svc *Mapfixes) SetStatusNotValidated(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) { MapfixID := int64(params.ID) // transaction target_status := model.MapfixStatusAcceptedUnvalidated @@ -255,6 +255,117 @@ func (svc *Mapfixes) SetStatusUploaded(ctx context.Context, params *validator.Ma return &validator.NullResponse{}, nil } +func (svc *Mapfixes) SetStatusNotUploaded(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) { + MapfixID := int64(params.ID) + // transaction + target_status := model.MapfixStatusValidated + update := service.NewMapfixUpdate() + update.SetStatusID(target_status) + allow_statuses := []model.MapfixStatus{model.MapfixStatusUploading} + err := svc.inner.UpdateMapfixIfStatus(ctx, MapfixID, allow_statuses, update) + if err != nil { + return nil, err + } + + // push an action audit event + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + err = svc.inner.CreateAuditEventAction( + ctx, + model.ValidatorUserID, + model.Resource{ + ID: MapfixID, + Type: model.ResourceMapfix, + }, + event_data, + ) + if err != nil { + return nil, err + } + + return &validator.NullResponse{}, nil +} + +// ActionMapfixReleased implements actionMapfixReleased operation. +// +// (Internal endpoint) Role Validator changes status from Releasing -> Released. +// +// POST /mapfixes/{MapfixID}/status/validator-released +func (svc *Mapfixes) SetStatusReleased(ctx context.Context, params *validator.MapfixReleaseRequest) (*validator.NullResponse, error) { + MapfixID := int64(params.MapfixID) + // transaction + target_status := model.MapfixStatusReleased + update := service.NewMapfixUpdate() + update.SetStatusID(target_status) + allow_statuses := []model.MapfixStatus{model.MapfixStatusReleasing} + err := svc.inner.UpdateMapfixIfStatus(ctx, MapfixID, allow_statuses, update) + if err != nil { + return nil, err + } + + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + err = svc.inner.CreateAuditEventAction( + ctx, + model.ValidatorUserID, + model.Resource{ + ID: MapfixID, + Type: model.ResourceMapfix, + }, + event_data, + ) + if err != nil { + return nil, err + } + + // metadata maintenance + map_update := service.NewMapUpdate() + map_update.SetAssetVersion(params.AssetVersion) + map_update.SetModes(params.Modes) + + err = svc.inner.UpdateMap(ctx, int64(params.TargetAssetID), map_update) + if err != nil { + return nil, err + } + + return &validator.NullResponse{}, nil +} +func (svc *Mapfixes) SetStatusNotReleased(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) { + MapfixID := int64(params.ID) + // transaction + target_status := model.MapfixStatusUploaded + update := service.NewMapfixUpdate() + update.SetStatusID(target_status) + allow_statuses := []model.MapfixStatus{model.MapfixStatusReleasing} + err := svc.inner.UpdateMapfixIfStatus(ctx, MapfixID, allow_statuses, update) + if err != nil { + return nil, err + } + + // push an action audit event + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + err = svc.inner.CreateAuditEventAction( + ctx, + model.ValidatorUserID, + model.Resource{ + ID: MapfixID, + Type: model.ResourceMapfix, + }, + event_data, + ) + if err != nil { + return nil, err + } + + return &validator.NullResponse{}, nil +} // CreateMapfixAuditError implements createMapfixAuditError operation. // diff --git a/pkg/validator_controller/submissions.go b/pkg/validator_controller/submissions.go index 44d043d..f897dd2 100644 --- a/pkg/validator_controller/submissions.go +++ b/pkg/validator_controller/submissions.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "git.itzana.me/strafesnet/go-grpc/validator" "git.itzana.me/strafesnet/maps-service/pkg/datastore" @@ -204,7 +205,7 @@ func (svc *Submissions) SetStatusValidated(ctx context.Context, params *validato // (Internal endpoint) Role Validator changes status from Validating -> Accepted. // // POST /submissions/{SubmissionID}/status/validator-failed -func (svc *Submissions) SetStatusFailed(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) { +func (svc *Submissions) SetStatusNotValidated(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) { SubmissionID := int64(params.ID) // transaction target_status := model.SubmissionStatusAcceptedUnvalidated @@ -275,6 +276,101 @@ func (svc *Submissions) SetStatusUploaded(ctx context.Context, params *validator return &validator.NullResponse{}, nil } +func (svc *Submissions) SetStatusNotUploaded(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) { + SubmissionID := int64(params.ID) + // transaction + target_status := model.SubmissionStatusValidated + update := service.NewSubmissionUpdate() + update.SetStatusID(target_status) + allowed_statuses :=[]model.SubmissionStatus{model.SubmissionStatusUploading} + err := svc.inner.UpdateSubmissionIfStatus(ctx, SubmissionID, allowed_statuses, update) + if err != nil { + return nil, err + } + + // push an action audit event + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + err = svc.inner.CreateAuditEventAction( + ctx, + model.ValidatorUserID, + model.Resource{ + ID: SubmissionID, + Type: model.ResourceSubmission, + }, + event_data, + ) + if err != nil { + return nil, err + } + + return &validator.NullResponse{}, nil +} + +func (svc *Submissions) SetStatusReleased(ctx context.Context, params *validator.SubmissionReleaseRequest) (*validator.NullResponse, error){ + // create map with go-grpc + _, err := svc.inner.CreateMap(ctx, model.Map{ + ID: params.MapCreate.ID, + DisplayName: params.MapCreate.DisplayName, + Creator: params.MapCreate.Creator, + GameID: params.MapCreate.GameID, + Date: time.Unix(params.MapCreate.Date, 0), + Submitter: params.MapCreate.Submitter, + Thumbnail: 0, + AssetVersion: params.MapCreate.AssetVersion, + LoadCount: 0, + Modes: params.MapCreate.Modes, + }) + if err != nil { + return nil, err + } + + // update status to Released + update := service.NewSubmissionUpdate() + update.SetStatusID(model.SubmissionStatusReleased) + err = svc.inner.UpdateSubmissionIfStatus(ctx, int64(params.SubmissionID), []model.SubmissionStatus{model.SubmissionStatusReleasing}, update) + if err != nil { + return nil, err + } + + return &validator.NullResponse{}, nil +} + +func (svc *Submissions) SetStatusNotReleased(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) { + SubmissionID := int64(params.ID) + // transaction + target_status := model.SubmissionStatusUploaded + update := service.NewSubmissionUpdate() + update.SetStatusID(target_status) + allowed_statuses :=[]model.SubmissionStatus{model.SubmissionStatusReleasing} + err := svc.inner.UpdateSubmissionIfStatus(ctx, SubmissionID, allowed_statuses, update) + if err != nil { + return nil, err + } + + // push an action audit event + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + err = svc.inner.CreateAuditEventAction( + ctx, + model.ValidatorUserID, + model.Resource{ + ID: SubmissionID, + Type: model.ResourceSubmission, + }, + event_data, + ) + if err != nil { + return nil, err + } + + return &validator.NullResponse{}, nil +} + // CreateSubmissionAuditError implements createSubmissionAuditError operation. // // Post an error to the audit log diff --git a/pkg/web_api/mapfixes.go b/pkg/web_api/mapfixes.go index f7d4cc4..b72d415 100644 --- a/pkg/web_api/mapfixes.go +++ b/pkg/web_api/mapfixes.go @@ -788,6 +788,127 @@ func (svc *Service) ActionMapfixValidated(ctx context.Context, params api.Action ) } +// ActionMapfixTriggerRelease invokes actionMapfixTriggerRelease operation. +// +// Role MapfixUpload changes status from Uploaded -> Releasing. +// +// POST /mapfixes/{MapfixID}/status/trigger-release +func (svc *Service) ActionMapfixTriggerRelease(ctx context.Context, params api.ActionMapfixTriggerReleaseParams) 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 ErrPermissionDeniedNeedRoleMapfixUpload + } + + userId, err := userInfo.GetUserID() + if err != nil { + return err + } + + // transaction + target_status := model.MapfixStatusReleasing + update := service.NewMapfixUpdate() + update.SetStatusID(target_status) + allow_statuses := []model.MapfixStatus{model.MapfixStatusUploaded} + mapfix, err := svc.inner.UpdateAndGetMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update) + if err != nil { + return err + } + + // this is a map fix + err = svc.inner.NatsReleaseMapfix( + mapfix.ID, + mapfix.ValidatedAssetID, + mapfix.ValidatedAssetVersion, + mapfix.TargetAssetID, + ) + if err != nil { + return err + } + + event_data := model.AuditEventDataAction{ + TargetStatus: uint32(target_status), + } + + return svc.inner.CreateAuditEventAction( + ctx, + userId, + model.Resource{ + ID: params.MapfixID, + Type: model.ResourceMapfix, + }, + event_data, + ) +} + +// ActionMapfixUploaded invokes actionMapfixUploaded operation. +// +// Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded. +// +// POST /mapfixes/{MapfixID}/status/reset-releasing +func (svc *Service) ActionMapfixUploaded(ctx context.Context, params api.ActionMapfixUploadedParams) 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 ErrPermissionDeniedNeedRoleMapfixUpload + } + + userId, err := userInfo.GetUserID() + if err != nil { + return err + } + + // check when mapfix was updated + mapfix, err := svc.inner.GetMapfix(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 + target_status := model.MapfixStatusUploaded + update := service.NewMapfixUpdate() + update.SetStatusID(target_status) + allow_statuses := []model.MapfixStatus{model.MapfixStatusReleasing} + err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_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.MapfixID, + Type: model.ResourceMapfix, + }, + event_data, + ) +} + // ActionMapfixTriggerValidate invokes actionMapfixTriggerValidate operation. // // Role Reviewer triggers validation and changes status from Submitted -> Validating. diff --git a/pkg/web_api/submissions.go b/pkg/web_api/submissions.go index da16abb..5b3e13e 100644 --- a/pkg/web_api/submissions.go +++ b/pkg/web_api/submissions.go @@ -1046,6 +1046,11 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas return ErrPermissionDeniedNeedRoleSubmissionRelease } + userId, err := userInfo.GetUserID() + if err != nil { + return err + } + idList := make([]int64, len(request)) for i, releaseInfo := range request { idList[i] = releaseInfo.SubmissionID @@ -1067,37 +1072,107 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas } } - for i,submission := range submissions{ - date := request[i].Date.Unix() - // create each map with go-grpc - _, err := svc.inner.CreateMap(ctx, model.Map{ - ID: int64(submission.UploadedAssetID), - DisplayName: submission.DisplayName, - Creator: submission.Creator, - GameID: submission.GameID, - Date: time.Unix(date, 0), - Submitter: submission.Submitter, - // Thumbnail: 0, - // AssetVersion: 0, - // LoadCount: 0, - // Modes: 0, - }) - if err != nil { - return err - } + // set every submission to Releasing status - // update each status to Released - update := service.NewSubmissionUpdate() - update.SetStatusID(model.SubmissionStatusReleased) - err = svc.inner.UpdateSubmissionIfStatus(ctx, submission.ID, []model.SubmissionStatus{model.SubmissionStatusUploaded}, update) - if err != nil { - return err - } + // construct batch release nats message + release_submissions := make([]model.ReleaseSubmissionRequest, len(request)) + for i, submission := range submissions { + // from request + release_submissions[i].ReleaseDate = request[i].Date.Unix() + release_submissions[i].SubmissionID = request[i].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 err + } + + // this is a map fix + err = svc.inner.NatsBatchReleaseSubmissions( + release_submissions, + operation.ID, + ) + if err != nil { + return err } return nil } +// ActionSubmissionUploaded invokes actionSubmissionUploaded operation. +// +// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> +// Uploaded. +// +// POST /submissions/{SubmissionID}/status/reset-releasing +func (svc *Service) ActionSubmissionUploaded(ctx context.Context, params api.ActionSubmissionUploadedParams) error { + userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) + if !ok { + return ErrUserInfo + } + + has_role, err := userInfo.HasRoleSubmissionRelease() + if err != nil { + return err + } + // check if caller has required role + if !has_role { + return ErrPermissionDeniedNeedRoleSubmissionRelease + } + + 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.SubmissionStatusUploaded + update := service.NewSubmissionUpdate() + update.SetStatusID(target_status) + allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusReleasing} + 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, + ) +} + // CreateSubmissionAuditComment implements createSubmissionAuditComment operation. // // Post a comment to the audit log From cfb7461c5afe15b788e5701f41696f8bd4081211 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 13 Aug 2025 19:17:37 -0700 Subject: [PATCH 08/27] validator: remove implicit map update --- validation/src/grpc/maps_extended.rs | 21 ------- validation/src/grpc/mod.rs | 1 - validation/src/main.rs | 2 - validation/src/message_handler.rs | 1 - validation/src/release_mapfix.rs | 0 validation/src/upload_mapfix.rs | 94 +--------------------------- 6 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 validation/src/grpc/maps_extended.rs create mode 100644 validation/src/release_mapfix.rs diff --git a/validation/src/grpc/maps_extended.rs b/validation/src/grpc/maps_extended.rs deleted file mode 100644 index 1c339ee..0000000 --- a/validation/src/grpc/maps_extended.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::endpoint; -use rust_grpc::maps_extended::*; -pub type ServiceClient=rust_grpc::maps_extended::maps_service_client::MapsServiceClient; -#[derive(Clone)] -pub struct Client{ - client:ServiceClient, -} -impl Client{ - pub fn new( - client:ServiceClient, - )->Self{ - Self{client} - } - // endpoint!(get,MapId,MapResponse); - // endpoint!(get_list,MapIdList,MapList); - endpoint!(update,MapUpdate,NullResponse); - // endpoint!(create,MapCreate,MapId); - // endpoint!(delete,MapId,NullResponse); - // endpoint!(list,ListRequest,MapList); - // endpoint!(increment_load_count,MapId,NullResponse); -} diff --git a/validation/src/grpc/mod.rs b/validation/src/grpc/mod.rs index c8e88a1..2fff8c9 100644 --- a/validation/src/grpc/mod.rs +++ b/validation/src/grpc/mod.rs @@ -1,7 +1,6 @@ pub mod error; -pub mod maps_extended; pub mod mapfixes; pub mod operations; pub mod scripts; diff --git a/validation/src/main.rs b/validation/src/main.rs index 9c58329..f36cacc 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -69,7 +69,6 @@ async fn main()->Result<(),StartupError>{ let scripts=crate::grpc::scripts::Service::new(crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone())); let script_policy=crate::grpc::script_policy::Service::new(crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone())); let submissions=crate::grpc::submissions::Service::new(crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel.clone())); - let maps_extended=crate::grpc::maps_extended::Client::new(crate::grpc::maps_extended::ServiceClient::new(channel)); let message_handler=message_handler::MessageHandler{ cloud_context, cookie_context, @@ -77,7 +76,6 @@ async fn main()->Result<(),StartupError>{ group_id, load_asset_version_place_id, load_asset_version_universe_id, - maps_extended, mapfixes, operations, scripts, diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 7e5a046..d62d153 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -34,7 +34,6 @@ pub struct MessageHandler{ pub(crate) group_id:Option, pub(crate) load_asset_version_place_id:u64, pub(crate) load_asset_version_universe_id:u64, - pub(crate) maps_extended:crate::grpc::maps_extended::Client, pub(crate) mapfixes:crate::grpc::mapfixes::Service, pub(crate) operations:crate::grpc::operations::Service, pub(crate) scripts:crate::grpc::scripts::Service, diff --git a/validation/src/release_mapfix.rs b/validation/src/release_mapfix.rs new file mode 100644 index 0000000..e69de29 diff --git a/validation/src/upload_mapfix.rs b/validation/src/upload_mapfix.rs index b0d0e27..e943922 100644 --- a/validation/src/upload_mapfix.rs +++ b/validation/src/upload_mapfix.rs @@ -9,21 +9,6 @@ pub enum Error{ Json(serde_json::Error), Upload(rbx_asset::cookie::UploadError), ApiActionMapfixUploaded(tonic::Status), - ModelFileDecode(crate::rbx_util::ReadDomError), - GetRootInstance(crate::rbx_util::GetRootInstanceError), - NonSequentialModes, - TooManyModes(usize), - CreateSession(rbx_asset::cloud::CreateError), - NonPositiveNumber(serde_json::Number), - Script(rbx_asset::cloud::LuauError), - InvalidResult(Vec), - LuauSession(rbx_asset::cloud::LuauSessionError), - GetAssetInfo(rbx_asset::cloud::GetError), - RevisionMismatch{ - after:u64, - before:u64, - }, - } impl std::fmt::Display for Error{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ @@ -44,95 +29,20 @@ impl crate::message_handler::MessageHandler{ let model_data=maybe_gzip.to_vec().map_err(Error::IO)?; // upload the map to the strafesnet group - let upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{ + let _upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{ assetid:upload_info.TargetAssetID, groupId:self.group_id, name:None, description:None, ispublic:None, allowComments:None, - },model_data.clone()).await.map_err(Error::Upload)?; + },model_data).await.map_err(Error::Upload)?; // mark mapfix as uploaded, TargetAssetID is unchanged self.mapfixes.set_status_uploaded(rust_grpc::validator::MapfixId{ id:upload_info.MapfixID, }).await.map_err(Error::ApiActionMapfixUploaded)?; - // count modes - - // decode dom (slow!) - let dom=crate::rbx_util::read_dom(model_data.as_slice()).map_err(Error::ModelFileDecode)?; - - // extract the root instance - let model_instance=crate::rbx_util::get_root_instance(&dom).map_err(Error::GetRootInstance)?; - - // extract information from the model - let model_info=crate::check::get_model_info(&dom,model_instance); - - // count modes - let modes=model_info.count_modes().ok_or(Error::NonSequentialModes)?; - - // hard limit LOL - let modes=if modesmatch results.as_slice(){ - [serde_json::Value::Number(load_asset_version)]=>load_asset_version.as_u64().ok_or_else(||Error::NonPositiveNumber(load_asset_version.clone())), - _=>Err(Error::InvalidResult(results)) - }, - Ok(Err(e))=>Err(Error::Script(e)), - Err(e)=>Err(Error::LuauSession(e)), - }?; - - // check asset version to make sure it hasn't been updated - let asset_response=self.cloud_context.get_asset_info(rbx_asset::cloud::GetAssetLatestRequest{ - asset_id:upload_info.TargetAssetID, - }).await.map_err(Error::GetAssetInfo)?; - - if upload_response.AssetVersion!=asset_response.revisionId{ - // the model was updated while we were fetching LoadAssetVersion. - // the number we got may be invalid. - return Err(Error::RevisionMismatch{ - before:upload_response.AssetVersion, - after:asset_response.revisionId, - }); - } - - // write AssetVersion directly to map - self.maps_extended.update(rust_grpc::maps_extended::MapUpdate{ - id:upload_info.TargetAssetID as i64, - asset_version:Some(load_asset_version), - modes:Some(modes), - display_name:None, - creator:None, - game_id:None, - date:None, - submitter:None, - thumbnail:None, - }).await.map_err(Error::ApiActionMapfixUploaded)?; - Ok(()) } } From 31cca0d45032709d7877c9f10676b5f8123c15d2 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 13 Aug 2025 18:55:56 -0700 Subject: [PATCH 09/27] validator: update rust-grpc --- Cargo.lock | 40 +++++++++++++++++---------- validation/Cargo.toml | 4 +-- validation/src/grpc/mapfixes.rs | 5 +++- validation/src/grpc/operations.rs | 1 + validation/src/grpc/submissions.rs | 5 +++- validation/src/validate_mapfix.rs | 2 +- validation/src/validate_submission.rs | 2 +- 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b2c52a..ba32c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,7 +784,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -1358,9 +1358,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -1368,9 +1368,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools", @@ -1381,9 +1381,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.5-serde3" +version = "0.14.1-serde2" source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" -checksum = "e42128b6e3a6655aa5f72ac65a33848a512eb9b23e98986adc4bbe6559ea88ce" +checksum = "c6bdb43aea117477820c164442f4e943ac7690d0dbe66cde45d78e0f7bb34386" dependencies = [ "prost", "serde", @@ -1441,7 +1441,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1734,14 +1734,15 @@ dependencies = [ [[package]] name = "rust-grpc" -version = "1.3.4" +version = "1.6.1" source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" -checksum = "ffab535c98c3a298cd092126036d5f8e40b9e600d24941823fb67a788be387ee" +checksum = "0793cf131a9c4746000533af36aadbfb34ec6877c9f1664f94c1a110df6628ce" dependencies = [ "prost", "prost-types", "serde", "tonic", + "tonic-prost", ] [[package]] @@ -2307,9 +2308,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.13.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +checksum = "67ac5a8627ada0968acec063a4746bf79588aa03ccb66db2f75d7dce26722a40" dependencies = [ "async-trait", "axum", @@ -2324,8 +2325,8 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", - "socket2 0.5.10", + "socket2 0.6.0", + "sync_wrapper", "tokio", "tokio-stream", "tower", @@ -2334,6 +2335,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9c511b9a96d40cb12b7d5d00464446acf3b9105fd3ce25437cfe41c92b1c87d" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" diff --git a/validation/Cargo.toml b/validation/Cargo.toml index fb58734..df64ac1 100644 --- a/validation/Cargo.toml +++ b/validation/Cargo.toml @@ -17,5 +17,5 @@ siphasher = "1.0.1" tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] } heck = "0.5.0" lazy-regex = "3.4.1" -rust-grpc = { version = "1.2.1", registry = "strafesnet" } -tonic = "0.13.1" +rust-grpc = { version = "1.6.1", registry = "strafesnet" } +tonic = "0.14.1" diff --git a/validation/src/grpc/mapfixes.rs b/validation/src/grpc/mapfixes.rs index aeee26e..325c9b0 100644 --- a/validation/src/grpc/mapfixes.rs +++ b/validation/src/grpc/mapfixes.rs @@ -18,6 +18,9 @@ impl Service{ endpoint!(set_status_submitted,SubmittedRequest,NullResponse); endpoint!(set_status_request_changes,MapfixId,NullResponse); endpoint!(set_status_validated,MapfixId,NullResponse); - endpoint!(set_status_failed,MapfixId,NullResponse); + endpoint!(set_status_not_validated,MapfixId,NullResponse); endpoint!(set_status_uploaded,MapfixId,NullResponse); + endpoint!(set_status_not_uploaded,MapfixId,NullResponse); + endpoint!(set_status_released,MapfixReleaseRequest,NullResponse); + endpoint!(set_status_not_released,MapfixId,NullResponse); } diff --git a/validation/src/grpc/operations.rs b/validation/src/grpc/operations.rs index 7c21522..942e258 100644 --- a/validation/src/grpc/operations.rs +++ b/validation/src/grpc/operations.rs @@ -11,5 +11,6 @@ impl Service{ )->Self{ Self{client} } + endpoint!(success,OperationSuccessRequest,NullResponse); endpoint!(fail,OperationFailRequest,NullResponse); } diff --git a/validation/src/grpc/submissions.rs b/validation/src/grpc/submissions.rs index 9dd7c75..0374ce7 100644 --- a/validation/src/grpc/submissions.rs +++ b/validation/src/grpc/submissions.rs @@ -18,6 +18,9 @@ impl Service{ endpoint!(set_status_submitted,SubmittedRequest,NullResponse); endpoint!(set_status_request_changes,SubmissionId,NullResponse); endpoint!(set_status_validated,SubmissionId,NullResponse); - endpoint!(set_status_failed,SubmissionId,NullResponse); + endpoint!(set_status_not_validated,SubmissionId,NullResponse); endpoint!(set_status_uploaded,StatusUploadedRequest,NullResponse); + endpoint!(set_status_not_uploaded,SubmissionId,NullResponse); + endpoint!(set_status_released,SubmissionReleaseRequest,NullResponse); + endpoint!(set_status_not_released,SubmissionId,NullResponse); } diff --git a/validation/src/validate_mapfix.rs b/validation/src/validate_mapfix.rs index 10cb0ef..70e8002 100644 --- a/validation/src/validate_mapfix.rs +++ b/validation/src/validate_mapfix.rs @@ -37,7 +37,7 @@ impl crate::message_handler::MessageHandler{ ).await.map_err(Error::ApiActionMapfixValidate)?; // update the mapfix model status to accepted - self.mapfixes.set_status_failed(rust_grpc::validator::MapfixId{ + self.mapfixes.set_status_not_validated(rust_grpc::validator::MapfixId{ id:mapfix_id, }).await.map_err(Error::ApiActionMapfixValidate)?; }, diff --git a/validation/src/validate_submission.rs b/validation/src/validate_submission.rs index 690e71d..7737263 100644 --- a/validation/src/validate_submission.rs +++ b/validation/src/validate_submission.rs @@ -37,7 +37,7 @@ impl crate::message_handler::MessageHandler{ ).await.map_err(Error::ApiActionSubmissionValidate)?; // update the submission model status to accepted - self.submissions.set_status_failed(rust_grpc::validator::SubmissionId{ + self.submissions.set_status_not_validated(rust_grpc::validator::SubmissionId{ id:submission_id, }).await.map_err(Error::ApiActionSubmissionValidate)?; }, From 412dadfc3ecf683971cf3c80609e844878f96da5 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 13 Aug 2025 19:17:43 -0700 Subject: [PATCH 10/27] validator: add mapfix and submission release --- validation/src/main.rs | 10 +- validation/src/message_handler.rs | 7 +- validation/src/nats_types.rs | 31 +++ validation/src/release.rs | 104 +++++++++ validation/src/release_mapfix.rs | 101 +++++++++ validation/src/release_submissions_batch.rs | 225 ++++++++++++++++++++ 6 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 validation/src/release.rs create mode 100644 validation/src/release_submissions_batch.rs diff --git a/validation/src/main.rs b/validation/src/main.rs index f36cacc..226ba71 100644 --- a/validation/src/main.rs +++ b/validation/src/main.rs @@ -13,6 +13,9 @@ mod check_submission; mod create; mod create_mapfix; mod create_submission; +mod release; +mod release_mapfix; +mod release_submissions_batch; mod upload_mapfix; mod upload_submission; mod validator; @@ -69,13 +72,16 @@ async fn main()->Result<(),StartupError>{ let scripts=crate::grpc::scripts::Service::new(crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone())); let script_policy=crate::grpc::script_policy::Service::new(crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone())); let submissions=crate::grpc::submissions::Service::new(crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel.clone())); + let load_asset_version_runtime=rbx_asset::cloud::LuauSessionLatestRequest{ + place_id:load_asset_version_place_id, + universe_id:load_asset_version_universe_id, + }; let message_handler=message_handler::MessageHandler{ cloud_context, cookie_context, cloud_context_luau_execution, group_id, - load_asset_version_place_id, - load_asset_version_universe_id, + load_asset_version_runtime, mapfixes, operations, scripts, diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index d62d153..bcb8065 100644 --- a/validation/src/message_handler.rs +++ b/validation/src/message_handler.rs @@ -9,6 +9,8 @@ pub enum HandleMessageError{ CreateSubmission(tonic::Status), CheckMapfix(crate::check_mapfix::Error), CheckSubmission(crate::check_submission::Error), + ReleaseMapfix(crate::release_mapfix::Error), + ReleaseSubmissionsBatch(crate::release_submissions_batch::Error), UploadMapfix(crate::upload_mapfix::Error), UploadSubmission(crate::upload_submission::Error), ValidateMapfix(crate::validate_mapfix::Error), @@ -32,8 +34,7 @@ pub struct MessageHandler{ pub(crate) cookie_context:rbx_asset::cookie::Context, pub(crate) cloud_context_luau_execution:rbx_asset::cloud::Context, pub(crate) group_id:Option, - pub(crate) load_asset_version_place_id:u64, - pub(crate) load_asset_version_universe_id:u64, + pub(crate) load_asset_version_runtime:rbx_asset::cloud::LuauSessionLatestRequest, pub(crate) mapfixes:crate::grpc::mapfixes::Service, pub(crate) operations:crate::grpc::operations::Service, pub(crate) scripts:crate::grpc::scripts::Service, @@ -50,6 +51,8 @@ impl MessageHandler{ "maptest.submissions.create"=>self.create_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::CreateSubmission), "maptest.mapfixes.check"=>self.check_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::CheckMapfix), "maptest.submissions.check"=>self.check_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::CheckSubmission), + "maptest.mapfixes.release"=>self.release_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::ReleaseMapfix), + "maptest.submissions.batchrelease"=>self.release_submissions_batch(from_slice(&message.payload)?).await.map_err(HandleMessageError::ReleaseSubmissionsBatch), "maptest.mapfixes.upload"=>self.upload_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::UploadMapfix), "maptest.submissions.upload"=>self.upload_submission(from_slice(&message.payload)?).await.map_err(HandleMessageError::UploadSubmission), "maptest.mapfixes.validate"=>self.validate_mapfix(from_slice(&message.payload)?).await.map_err(HandleMessageError::ValidateMapfix), diff --git a/validation/src/nats_types.rs b/validation/src/nats_types.rs index 2c29cd3..19f2a99 100644 --- a/validation/src/nats_types.rs +++ b/validation/src/nats_types.rs @@ -81,3 +81,34 @@ pub struct UploadMapfixRequest{ pub ModelVersion:u64, pub TargetAssetID:u64, } + +// Release a new map +#[allow(nonstandard_style)] +#[derive(serde::Deserialize)] +pub struct ReleaseSubmissionRequest{ + pub SubmissionID:u64, + pub ReleaseDate:i64, + pub ModelID:u64, + pub ModelVersion:u64, + pub UploadedAssetID:u64, + pub DisplayName:String, + pub Creator:String, + pub GameID:u32, + pub Submitter:u64, +} + +#[allow(nonstandard_style)] +#[derive(serde::Deserialize)] +pub struct ReleaseSubmissionsBatchRequest{ + pub Submissions:Vec, + pub OperationID:u32, +} + +#[allow(nonstandard_style)] +#[derive(serde::Deserialize)] +pub struct ReleaseMapfixRequest{ + pub MapfixID:u64, + pub ModelID:u64, + pub ModelVersion:u64, + pub TargetAssetID:u64, +} diff --git a/validation/src/release.rs b/validation/src/release.rs new file mode 100644 index 0000000..e315e60 --- /dev/null +++ b/validation/src/release.rs @@ -0,0 +1,104 @@ +use crate::rbx_util::read_dom; + +#[expect(unused)] +#[derive(Debug)] +pub enum ModesError{ + ApiActionMapfixReleased(tonic::Status), + ModelFileDecode(crate::rbx_util::ReadDomError), + GetRootInstance(crate::rbx_util::GetRootInstanceError), + NonSequentialModes, + TooManyModes(usize), +} +impl std::fmt::Display for ModesError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for ModesError{} + +// decode and get modes function +pub fn count_modes(maybe_gzip:rbx_asset::types::MaybeGzippedBytes)->Result{ + // decode dom (slow!) + let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(ModesError::ModelFileDecode)?; + + // extract the root instance + let model_instance=crate::rbx_util::get_root_instance(&dom).map_err(ModesError::GetRootInstance)?; + + // extract information from the model + let model_info=crate::check::get_model_info(&dom,model_instance); + + // count modes + let modes=model_info.count_modes().ok_or(ModesError::NonSequentialModes)?; + + // hard limit LOL + let modes=if modes), + LuauSession(rbx_asset::cloud::LuauSessionError), +} +impl std::fmt::Display for LoadAssetVersionsError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for LoadAssetVersionsError{} + +// get asset versions in bulk using Roblox Luau API +pub async fn load_asset_versions>( + context:&rbx_asset::cloud::Context, + runtime:&rbx_asset::cloud::LuauSessionLatestRequest, + assets:I, +)->Result,LoadAssetVersionsError>{ + // construct script with inline IDs + // TODO: concurrent execution + let mut script="local InsertService=game:GetService(\"InsertService\")\n".to_string(); + script+="return {\n"; + for asset in assets{ + script+="InsertService:GetLatestAssetVersionAsync("; + script+=&asset.to_string(); + script+="),\n"; + } + script+="}\n"; + + let session=rbx_asset::cloud::LuauSessionCreate{ + script:&script, + user:None, + timeout:None, + binaryInput:None, + enableBinaryOutput:None, + binaryOutputUri:None, + }; + let session_response=context.create_luau_session(runtime,session).await.map_err(LoadAssetVersionsError::CreateSession)?; + + let result=crate::rbx_util::get_luau_result_exp_backoff(&context,&session_response).await; + + // * Note that only one mapfix can be active per map + // * so it's theoretically impossible for the map to be updated unexpectedly. + // * This means that the incremental asset version does not + // * need to be checked before and after the load asset version is checked. + + match result{ + Ok(Ok(rbx_asset::cloud::LuauResults{results}))=>{ + results.into_iter().map(|load_asset_version| + load_asset_version.as_u64().ok_or_else(||LoadAssetVersionsError::NonPositiveNumber(load_asset_version.clone())) + ).collect() + }, + Ok(Err(e))=>Err(LoadAssetVersionsError::Script(e)), + Err(e)=>Err(LoadAssetVersionsError::LuauSession(e)), + } + + // * Don't need to check asset version to make sure it hasn't been updated +} diff --git a/validation/src/release_mapfix.rs b/validation/src/release_mapfix.rs index e69de29..0940a49 100644 --- a/validation/src/release_mapfix.rs +++ b/validation/src/release_mapfix.rs @@ -0,0 +1,101 @@ +use crate::download::download_asset_version; +use crate::nats_types::ReleaseMapfixRequest; +use crate::release::{count_modes,load_asset_versions}; + +#[expect(unused)] +#[derive(Debug)] +pub enum InnerError{ + Download(crate::download::Error), + Modes(crate::release::ModesError), + LoadAssetVersions(crate::release::LoadAssetVersionsError), + LoadAssetVersionsListLength, +} +impl std::fmt::Display for InnerError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for InnerError{} + +async fn release_inner( + cloud_context:&rbx_asset::cloud::Context, + cloud_context_luau_execution:&rbx_asset::cloud::Context, + load_asset_version_runtime:&rbx_asset::cloud::LuauSessionLatestRequest, + release_info:ReleaseMapfixRequest, +)->Result{ + // download the map model + let maybe_gzip=download_asset_version(cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ + asset_id:release_info.ModelID, + version:release_info.ModelVersion, + }).await.map_err(InnerError::Download)?; + + // count modes + let modes=count_modes(maybe_gzip).map_err(InnerError::Modes)?; + + // fetch load asset version + let load_asset_versions=load_asset_versions( + cloud_context_luau_execution, + load_asset_version_runtime, + [release_info.TargetAssetID], + ).await.map_err(InnerError::LoadAssetVersions)?; + + // exactly one value in the results + let &[load_asset_version]=load_asset_versions.as_slice()else{ + return Err(InnerError::LoadAssetVersionsListLength); + }; + + Ok(rust_grpc::validator::MapfixReleaseRequest{ + mapfix_id:release_info.MapfixID, + target_asset_id:release_info.TargetAssetID, + asset_version:load_asset_version, + modes:modes, + }) +} + +#[expect(unused)] +#[derive(Debug)] +pub enum Error{ + ApiActionMapfixRelease(tonic::Status), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +impl crate::message_handler::MessageHandler{ + pub async fn release_mapfix(&self,release_info:ReleaseMapfixRequest)->Result<(),Error>{ + let mapfix_id=release_info.MapfixID; + let result=release_inner( + &self.cloud_context, + &self.cloud_context_luau_execution, + &self.load_asset_version_runtime, + release_info, + ).await; + + match result{ + Ok(request)=>{ + // update map metadata + self.mapfixes.set_status_released(request).await.map_err(Error::ApiActionMapfixRelease)?; + }, + Err(e)=>{ + // log error + println!("[release_mapfix] Error: {e}"); + + // post an error message to the audit log + self.mapfixes.create_audit_error(rust_grpc::validator::AuditErrorRequest{ + id:mapfix_id, + error_message:e.to_string(), + }).await.map_err(Error::ApiActionMapfixRelease)?; + + // update the mapfix model status to uploaded + self.mapfixes.set_status_not_released(rust_grpc::validator::MapfixId{ + id:mapfix_id, + }).await.map_err(Error::ApiActionMapfixRelease)?; + }, + } + + Ok(()) + } +} diff --git a/validation/src/release_submissions_batch.rs b/validation/src/release_submissions_batch.rs new file mode 100644 index 0000000..5eed427 --- /dev/null +++ b/validation/src/release_submissions_batch.rs @@ -0,0 +1,225 @@ +use futures::StreamExt; + +use crate::download::download_asset_version; +use crate::nats_types::ReleaseSubmissionsBatchRequest; +use crate::release::{count_modes,load_asset_versions}; + + +#[expect(unused)] +#[derive(Debug)] +pub enum DownloadFutError{ + Download(crate::download::Error), + Join(tokio::task::JoinError), + Modes(crate::release::ModesError), +} +impl std::fmt::Display for DownloadFutError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for DownloadFutError{} + +#[derive(Debug)] +pub struct ErrorContext{ + submission_id:u64, + error:E, +} +impl std::fmt::Display for ErrorContext{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"SubmissionID({})={:?}",self.submission_id,self.error) + } +} +impl std::error::Error for ErrorContext{} + +async fn download_fut( + cloud_context:&rbx_asset::cloud::Context, + asset_version:rbx_asset::cloud::GetAssetVersionRequest, +)->Result{ + // download + let maybe_gzip=download_asset_version(cloud_context,asset_version) + .await + .map_err(DownloadFutError::Download)?; + + // count modes in a green thread + let modes=tokio::task::spawn_blocking(|| + count_modes(maybe_gzip) + ) + .await + .map_err(DownloadFutError::Join)? + .map_err(DownloadFutError::Modes)?; + + Ok::<_,DownloadFutError>(modes) +} + +#[expect(unused)] +#[derive(Debug)] +pub enum InnerError{ + Io(std::io::Error), + LoadAssetVersions(crate::release::LoadAssetVersionsError), + LoadAssetVersionsListLength, + DownloadFutErrors(Vec>), + ReleaseErrors(Vec>), +} +impl std::fmt::Display for InnerError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for InnerError{} + +const MAX_PARALLEL_DECODE:usize=6; +const MAX_CONCURRENT_RELEASE:usize=16; + +async fn release_inner( + release_info:ReleaseSubmissionsBatchRequest, + cloud_context:&rbx_asset::cloud::Context, + cloud_context_luau_execution:&rbx_asset::cloud::Context, + load_asset_version_runtime:&rbx_asset::cloud::LuauSessionLatestRequest, + submissions:&crate::grpc::submissions::Service, +)->Result<(),InnerError>{ + let available_parallelism=std::thread::available_parallelism().map_err(InnerError::Io)?.get(); + // set up futures + + // fut_download + let fut_download=futures::stream::iter( + release_info + .Submissions + .iter() + .enumerate() + .map(|(index,submission)|async move{ + let asset_version=rbx_asset::cloud::GetAssetVersionRequest{ + asset_id:submission.ModelID, + version:submission.ModelVersion, + }; + let modes=download_fut(cloud_context,asset_version).await; + (index,modes) + }) + ) + .buffer_unordered(available_parallelism.min(MAX_PARALLEL_DECODE)) + .collect::)>>(); + + // fut_luau + let fut_load_asset_versions=load_asset_versions( + cloud_context_luau_execution, + load_asset_version_runtime, + release_info.Submissions.iter().map(|submission|submission.UploadedAssetID), + ); + + // execute futures + let (mut modes_unordered,load_asset_versions_result)=tokio::join!(fut_download,fut_load_asset_versions); + + let load_asset_versions=load_asset_versions_result.map_err(InnerError::LoadAssetVersions)?; + + // sanity check roblox output + if load_asset_versions.len()!=release_info.Submissions.len(){ + return Err(InnerError::LoadAssetVersionsListLength); + }; + + // rip asymptotic complexity (hash map would be better) + modes_unordered.sort_by_key(|&(index,_)|index); + + // check modes calculations for all success + let mut modes=Vec::with_capacity(modes_unordered.len()); + let mut errors=Vec::with_capacity(modes_unordered.len()); + for (index,result) in modes_unordered{ + match result{ + Ok(value)=>modes.push(value), + Err(error)=>errors.push(ErrorContext{ + submission_id:release_info.Submissions[index].SubmissionID, + error:error, + }), + } + } + if !errors.is_empty(){ + return Err(InnerError::DownloadFutErrors(errors)); + } + + // concurrently dispatch results + let release_results:Vec<_> =futures::stream::iter( + release_info + .Submissions + .into_iter() + .zip(modes) + .zip(load_asset_versions) + .map(|((submission,modes),asset_version)|async move{ + let result=submissions.set_status_released(rust_grpc::validator::SubmissionReleaseRequest{ + submission_id:submission.SubmissionID, + map_create:Some(rust_grpc::maps_extended::MapCreate{ + id:submission.UploadedAssetID as i64, + display_name:submission.DisplayName, + creator:submission.Creator, + game_id:submission.GameID, + date:submission.ReleaseDate, + submitter:submission.Submitter, + thumbnail:0, + asset_version, + modes, + }), + }).await; + (submission.SubmissionID,result) + }) + ) + .buffer_unordered(MAX_CONCURRENT_RELEASE) + .collect().await; + + // check for errors + let errors:Vec<_> = + release_results + .into_iter() + .filter_map(|(submission_id,result)| + result.err().map(|e|ErrorContext{ + submission_id, + error:e, + }) + ) + .collect(); + + if !errors.is_empty(){ + return Err(InnerError::ReleaseErrors(errors)); + } + + Ok(()) +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ + UpdateOperation(tonic::Status), +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +impl crate::message_handler::MessageHandler{ + pub async fn release_submissions_batch(&self,release_info:ReleaseSubmissionsBatchRequest)->Result<(),Error>{ + let operation_id=release_info.OperationID; + let result=release_inner( + release_info, + &self.cloud_context, + &self.cloud_context_luau_execution, + &self.load_asset_version_runtime, + &self.submissions, + ).await; + + match result{ + Ok(())=>{ + // operation success + self.operations.success(rust_grpc::validator::OperationSuccessRequest{ + operation_id, + path:String::new(), + }).await.map_err(Error::UpdateOperation)?; + }, + Err(e)=>{ + // operation error + self.operations.fail(rust_grpc::validator::OperationFailRequest{ + operation_id, + status_message:e.to_string(), + }).await.map_err(Error::UpdateOperation)?; + }, + } + Ok(()) + } +} From 76512bec0dd561e7b7038db925ff91f384fffa41 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 13 Aug 2025 19:16:39 -0700 Subject: [PATCH 11/27] validator: update deps --- Cargo.lock | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba32c88..8dfc141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,9 +56,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arrayref" @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.31" +version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ "jobserver", "libc", @@ -640,9 +640,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -659,9 +659,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "heck" @@ -1026,9 +1026,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "litemap" @@ -1330,9 +1330,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] @@ -1403,7 +1403,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.5.10", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tracing", "web-time", @@ -1424,7 +1424,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.14", "tinyvec", "tracing", "web-time", @@ -1655,9 +1655,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -1835,9 +1835,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2009,9 +2009,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -2075,9 +2075,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" dependencies = [ "proc-macro2", "quote", @@ -2136,11 +2136,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.14", ] [[package]] @@ -2156,9 +2156,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", From e90cc425ba8ac721a61903cf08805f7f038e9128 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 14 Aug 2025 18:21:46 -0700 Subject: [PATCH 12/27] validator: perform unnecessary allocation to appease borrow checker --- validation/src/release_submissions_batch.rs | 30 +++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/validation/src/release_submissions_batch.rs b/validation/src/release_submissions_batch.rs index 5eed427..8d77eef 100644 --- a/validation/src/release_submissions_batch.rs +++ b/validation/src/release_submissions_batch.rs @@ -80,21 +80,23 @@ async fn release_inner( let available_parallelism=std::thread::available_parallelism().map_err(InnerError::Io)?.get(); // set up futures + // unnecessary allocation :( + let asset_versions:Vec<_> =release_info + .Submissions + .iter() + .map(|submission|rbx_asset::cloud::GetAssetVersionRequest{ + asset_id:submission.ModelID, + version:submission.ModelVersion, + }) + .enumerate() + .collect(); + // fut_download - let fut_download=futures::stream::iter( - release_info - .Submissions - .iter() - .enumerate() - .map(|(index,submission)|async move{ - let asset_version=rbx_asset::cloud::GetAssetVersionRequest{ - asset_id:submission.ModelID, - version:submission.ModelVersion, - }; - let modes=download_fut(cloud_context,asset_version).await; - (index,modes) - }) - ) + let fut_download=futures::stream::iter(asset_versions) + .map(|(index,asset_version)|async move{ + let modes=download_fut(cloud_context,asset_version).await; + (index,modes) + }) .buffer_unordered(available_parallelism.min(MAX_PARALLEL_DECODE)) .collect::)>>(); From 0d18167b03eb8bea9391db98e506e4179ae2d5e4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 14 Aug 2025 18:30:32 -0700 Subject: [PATCH 13/27] remove SubmissionStatusReleasing --- openapi.yaml | 17 ------- pkg/model/submission.go | 1 - pkg/validator_controller/submissions.go | 37 +-------------- pkg/web_api/submissions.go | 63 ------------------------- validation/src/grpc/submissions.rs | 1 - web/src/app/ts/Status.ts | 26 +++++----- web/src/app/ts/Submission.ts | 3 -- 7 files changed, 14 insertions(+), 134 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 76c7028..e02f122 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1133,23 +1133,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" - /submissions/{SubmissionID}/status/reset-releasing: - post: - summary: Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> Uploaded - operationId: actionSubmissionUploaded - tags: - - Submissions - parameters: - - $ref: '#/components/parameters/SubmissionID' - responses: - "204": - description: Successful response - default: - description: General Error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" /release-submissions: post: summary: Release a set of uploaded maps. Role SubmissionRelease diff --git a/pkg/model/submission.go b/pkg/model/submission.go index 2b559a9..4840e5e 100644 --- a/pkg/model/submission.go +++ b/pkg/model/submission.go @@ -19,7 +19,6 @@ const ( SubmissionStatusValidated SubmissionStatus = 6 SubmissionStatusUploading SubmissionStatus = 7 SubmissionStatusUploaded SubmissionStatus = 8 // uploaded to the group, but pending release - SubmissionStatusReleasing SubmissionStatus = 11 // Phase: Final SubmissionStatus SubmissionStatusRejected SubmissionStatus = 9 diff --git a/pkg/validator_controller/submissions.go b/pkg/validator_controller/submissions.go index f897dd2..999fa02 100644 --- a/pkg/validator_controller/submissions.go +++ b/pkg/validator_controller/submissions.go @@ -27,8 +27,6 @@ func NewSubmissionsController( var( // prevent two submissions with same asset id ActiveSubmissionStatuses = []model.SubmissionStatus{ - model.SubmissionStatusReleasing, - model.SubmissionStatusUploaded, model.SubmissionStatusUploading, model.SubmissionStatusValidated, model.SubmissionStatusValidating, @@ -330,40 +328,7 @@ func (svc *Submissions) SetStatusReleased(ctx context.Context, params *validator // update status to Released update := service.NewSubmissionUpdate() update.SetStatusID(model.SubmissionStatusReleased) - err = svc.inner.UpdateSubmissionIfStatus(ctx, int64(params.SubmissionID), []model.SubmissionStatus{model.SubmissionStatusReleasing}, update) - if err != nil { - return nil, err - } - - return &validator.NullResponse{}, nil -} - -func (svc *Submissions) SetStatusNotReleased(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) { - SubmissionID := int64(params.ID) - // transaction - target_status := model.SubmissionStatusUploaded - update := service.NewSubmissionUpdate() - update.SetStatusID(target_status) - allowed_statuses :=[]model.SubmissionStatus{model.SubmissionStatusReleasing} - err := svc.inner.UpdateSubmissionIfStatus(ctx, SubmissionID, allowed_statuses, update) - if err != nil { - return nil, err - } - - // push an action audit event - event_data := model.AuditEventDataAction{ - TargetStatus: uint32(target_status), - } - - err = svc.inner.CreateAuditEventAction( - ctx, - model.ValidatorUserID, - model.Resource{ - ID: SubmissionID, - Type: model.ResourceSubmission, - }, - event_data, - ) + err = svc.inner.UpdateSubmissionIfStatus(ctx, int64(params.SubmissionID), []model.SubmissionStatus{model.SubmissionStatusUploaded}, update) if err != nil { return nil, err } diff --git a/pkg/web_api/submissions.go b/pkg/web_api/submissions.go index 5b3e13e..5eed163 100644 --- a/pkg/web_api/submissions.go +++ b/pkg/web_api/submissions.go @@ -1072,8 +1072,6 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas } } - // set every submission to Releasing status - // construct batch release nats message release_submissions := make([]model.ReleaseSubmissionRequest, len(request)) for i, submission := range submissions { @@ -1112,67 +1110,6 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas return nil } -// ActionSubmissionUploaded invokes actionSubmissionUploaded operation. -// -// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> -// Uploaded. -// -// POST /submissions/{SubmissionID}/status/reset-releasing -func (svc *Service) ActionSubmissionUploaded(ctx context.Context, params api.ActionSubmissionUploadedParams) error { - userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) - if !ok { - return ErrUserInfo - } - - has_role, err := userInfo.HasRoleSubmissionRelease() - if err != nil { - return err - } - // check if caller has required role - if !has_role { - return ErrPermissionDeniedNeedRoleSubmissionRelease - } - - 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.SubmissionStatusUploaded - update := service.NewSubmissionUpdate() - update.SetStatusID(target_status) - allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusReleasing} - 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, - ) -} - // CreateSubmissionAuditComment implements createSubmissionAuditComment operation. // // Post a comment to the audit log diff --git a/validation/src/grpc/submissions.rs b/validation/src/grpc/submissions.rs index 0374ce7..e032831 100644 --- a/validation/src/grpc/submissions.rs +++ b/validation/src/grpc/submissions.rs @@ -22,5 +22,4 @@ impl Service{ endpoint!(set_status_uploaded,StatusUploadedRequest,NullResponse); endpoint!(set_status_not_uploaded,SubmissionId,NullResponse); endpoint!(set_status_released,SubmissionReleaseRequest,NullResponse); - endpoint!(set_status_not_released,SubmissionId,NullResponse); } diff --git a/web/src/app/ts/Status.ts b/web/src/app/ts/Status.ts index 83c3a4d..2700ff0 100644 --- a/web/src/app/ts/Status.ts +++ b/web/src/app/ts/Status.ts @@ -1,18 +1,18 @@ -import {SubmissionStatus} from "@/app/ts/Submission"; +import {MapfixStatus} from "@/app/ts/Mapfix"; export const Status = { - UnderConstruction: SubmissionStatus.UnderConstruction, - ChangesRequested: SubmissionStatus.ChangesRequested, - Submitting: SubmissionStatus.Submitting, - Submitted: SubmissionStatus.Submitted, - AcceptedUnvalidated: SubmissionStatus.AcceptedUnvalidated, - Validating: SubmissionStatus.Validating, - Validated: SubmissionStatus.Validated, - Uploading: SubmissionStatus.Uploading, - Uploaded: SubmissionStatus.Uploaded, - Rejected: SubmissionStatus.Rejected, - Release: SubmissionStatus.Released, - Releasing: SubmissionStatus.Releasing, + UnderConstruction: MapfixStatus.UnderConstruction, + ChangesRequested: MapfixStatus.ChangesRequested, + Submitting: MapfixStatus.Submitting, + Submitted: MapfixStatus.Submitted, + AcceptedUnvalidated: MapfixStatus.AcceptedUnvalidated, + Validating: MapfixStatus.Validating, + Validated: MapfixStatus.Validated, + Uploading: MapfixStatus.Uploading, + Uploaded: MapfixStatus.Uploaded, + Rejected: MapfixStatus.Rejected, + Release: MapfixStatus.Released, + Releasing: MapfixStatus.Releasing, }; export const StatusMatches = (status: number, statusValues: number[]) => { diff --git a/web/src/app/ts/Submission.ts b/web/src/app/ts/Submission.ts index 2e0f6ee..4d7b18d 100644 --- a/web/src/app/ts/Submission.ts +++ b/web/src/app/ts/Submission.ts @@ -8,7 +8,6 @@ const enum SubmissionStatus { Validated = 6, Uploading = 7, Uploaded = 8, - Releasing = 11, Rejected = 9, Released = 10, } @@ -40,8 +39,6 @@ function SubmissionStatusToString(submission_status: SubmissionStatus): string { return "RELEASED" case SubmissionStatus.Rejected: return "REJECTED" - case SubmissionStatus.Releasing: - return "RELEASING" case SubmissionStatus.Uploading: return "UPLOADING" case SubmissionStatus.Uploaded: From 10507c62ab6c34af3750fe67f62acc31340327b2 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 14 Aug 2025 18:30:50 -0700 Subject: [PATCH 14/27] openapi: generate --- pkg/api/oas_client_gen.go | 132 ------------------ pkg/api/oas_handlers_gen.go | 196 --------------------------- pkg/api/oas_operations_gen.go | 1 - pkg/api/oas_parameters_gen.go | 83 ------------ pkg/api/oas_response_decoders_gen.go | 60 -------- pkg/api/oas_response_encoders_gen.go | 7 - pkg/api/oas_router_gen.go | 46 ------- pkg/api/oas_schemas_gen.go | 3 - pkg/api/oas_security_gen.go | 1 - pkg/api/oas_server_gen.go | 7 - pkg/api/oas_unimplemented_gen.go | 10 -- 11 files changed, 546 deletions(-) diff --git a/pkg/api/oas_client_gen.go b/pkg/api/oas_client_gen.go index f081c32..7f760bd 100644 --- a/pkg/api/oas_client_gen.go +++ b/pkg/api/oas_client_gen.go @@ -169,13 +169,6 @@ type Invoker interface { // // POST /submissions/{SubmissionID}/status/trigger-validate ActionSubmissionTriggerValidate(ctx context.Context, params ActionSubmissionTriggerValidateParams) error - // ActionSubmissionUploaded invokes actionSubmissionUploaded operation. - // - // Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> - // Uploaded. - // - // POST /submissions/{SubmissionID}/status/reset-releasing - ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error // ActionSubmissionValidated invokes actionSubmissionValidated operation. // // Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> @@ -3286,131 +3279,6 @@ func (c *Client) sendActionSubmissionTriggerValidate(ctx context.Context, params return result, nil } -// ActionSubmissionUploaded invokes actionSubmissionUploaded operation. -// -// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> -// Uploaded. -// -// POST /submissions/{SubmissionID}/status/reset-releasing -func (c *Client) ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error { - _, err := c.sendActionSubmissionUploaded(ctx, params) - return err -} - -func (c *Client) sendActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) (res *ActionSubmissionUploadedNoContent, err error) { - otelAttrs := []attribute.KeyValue{ - otelogen.OperationID("actionSubmissionUploaded"), - semconv.HTTPRequestMethodKey.String("POST"), - semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reset-releasing"), - } - - // Run stopwatch. - startTime := time.Now() - defer func() { - // Use floating point division here for higher precision (instead of Millisecond method). - elapsedDuration := time.Since(startTime) - c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) - }() - - // Increment request counter. - c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) - - // Start a span for this request. - ctx, span := c.cfg.Tracer.Start(ctx, ActionSubmissionUploadedOperation, - trace.WithAttributes(otelAttrs...), - clientSpanKind, - ) - // Track stage for error reporting. - var stage string - defer func() { - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, stage) - c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) - } - span.End() - }() - - stage = "BuildURL" - u := uri.Clone(c.requestURL(ctx)) - var pathParts [3]string - pathParts[0] = "/submissions/" - { - // Encode "SubmissionID" parameter. - e := uri.NewPathEncoder(uri.PathEncoderConfig{ - Param: "SubmissionID", - Style: uri.PathStyleSimple, - Explode: false, - }) - if err := func() error { - return e.EncodeValue(conv.Int64ToString(params.SubmissionID)) - }(); err != nil { - return res, errors.Wrap(err, "encode path") - } - encoded, err := e.Result() - if err != nil { - return res, errors.Wrap(err, "encode path") - } - pathParts[1] = encoded - } - pathParts[2] = "/status/reset-releasing" - uri.AddPathParts(u, pathParts[:]...) - - stage = "EncodeRequest" - r, err := ht.NewRequest(ctx, "POST", u) - if err != nil { - return res, errors.Wrap(err, "create request") - } - - { - type bitset = [1]uint8 - var satisfied bitset - { - stage = "Security:CookieAuth" - switch err := c.securityCookieAuth(ctx, ActionSubmissionUploadedOperation, r); { - case err == nil: // if NO error - satisfied[0] |= 1 << 0 - case errors.Is(err, ogenerrors.ErrSkipClientSecurity): - // Skip this security. - default: - return res, errors.Wrap(err, "security \"CookieAuth\"") - } - } - - if ok := func() bool { - nextRequirement: - for _, requirement := range []bitset{ - {0b00000001}, - } { - for i, mask := range requirement { - if satisfied[i]&mask != mask { - continue nextRequirement - } - } - return true - } - return false - }(); !ok { - return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied - } - } - - stage = "SendRequest" - resp, err := c.cfg.Client.Do(r) - if err != nil { - return res, errors.Wrap(err, "do request") - } - defer resp.Body.Close() - - stage = "DecodeResponse" - result, err := decodeActionSubmissionUploadedResponse(resp) - if err != nil { - return res, errors.Wrap(err, "decode response") - } - - return result, nil -} - // ActionSubmissionValidated invokes actionSubmissionValidated operation. // // Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> diff --git a/pkg/api/oas_handlers_gen.go b/pkg/api/oas_handlers_gen.go index 9b02f6e..d1d2d6e 100644 --- a/pkg/api/oas_handlers_gen.go +++ b/pkg/api/oas_handlers_gen.go @@ -4517,202 +4517,6 @@ func (s *Server) handleActionSubmissionTriggerValidateRequest(args [1]string, ar } } -// handleActionSubmissionUploadedRequest handles actionSubmissionUploaded operation. -// -// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> -// Uploaded. -// -// POST /submissions/{SubmissionID}/status/reset-releasing -func (s *Server) handleActionSubmissionUploadedRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { - statusWriter := &codeRecorder{ResponseWriter: w} - w = statusWriter - otelAttrs := []attribute.KeyValue{ - otelogen.OperationID("actionSubmissionUploaded"), - semconv.HTTPRequestMethodKey.String("POST"), - semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reset-releasing"), - } - - // Start a span for this request. - ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionUploadedOperation, - trace.WithAttributes(otelAttrs...), - serverSpanKind, - ) - defer span.End() - - // Add Labeler to context. - labeler := &Labeler{attrs: otelAttrs} - ctx = contextWithLabeler(ctx, labeler) - - // Run stopwatch. - startTime := time.Now() - defer func() { - elapsedDuration := time.Since(startTime) - - attrSet := labeler.AttributeSet() - attrs := attrSet.ToSlice() - code := statusWriter.status - if code != 0 { - codeAttr := semconv.HTTPResponseStatusCode(code) - attrs = append(attrs, codeAttr) - span.SetAttributes(codeAttr) - } - attrOpt := metric.WithAttributes(attrs...) - - // Increment request counter. - s.requests.Add(ctx, 1, attrOpt) - - // Use floating point division here for higher precision (instead of Millisecond method). - s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) - }() - - var ( - recordError = func(stage string, err error) { - span.RecordError(err) - - // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status - // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, - // unless there was another error (e.g., network error receiving the response body; or 3xx codes with - // max redirects exceeded), in which case status MUST be set to Error. - code := statusWriter.status - if code >= 100 && code < 500 { - span.SetStatus(codes.Error, stage) - } - - attrSet := labeler.AttributeSet() - attrs := attrSet.ToSlice() - if code != 0 { - attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) - } - - s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) - } - err error - opErrContext = ogenerrors.OperationContext{ - Name: ActionSubmissionUploadedOperation, - ID: "actionSubmissionUploaded", - } - ) - { - type bitset = [1]uint8 - var satisfied bitset - { - sctx, ok, err := s.securityCookieAuth(ctx, ActionSubmissionUploadedOperation, r) - if err != nil { - err = &ogenerrors.SecurityError{ - OperationContext: opErrContext, - Security: "CookieAuth", - Err: err, - } - if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { - defer recordError("Security:CookieAuth", err) - } - return - } - if ok { - satisfied[0] |= 1 << 0 - ctx = sctx - } - } - - if ok := func() bool { - nextRequirement: - for _, requirement := range []bitset{ - {0b00000001}, - } { - for i, mask := range requirement { - if satisfied[i]&mask != mask { - continue nextRequirement - } - } - return true - } - return false - }(); !ok { - err = &ogenerrors.SecurityError{ - OperationContext: opErrContext, - Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, - } - if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { - defer recordError("Security", err) - } - return - } - } - params, err := decodeActionSubmissionUploadedParams(args, argsEscaped, r) - if err != nil { - err = &ogenerrors.DecodeParamsError{ - OperationContext: opErrContext, - Err: err, - } - defer recordError("DecodeParams", err) - s.cfg.ErrorHandler(ctx, w, r, err) - return - } - - var response *ActionSubmissionUploadedNoContent - if m := s.cfg.Middleware; m != nil { - mreq := middleware.Request{ - Context: ctx, - OperationName: ActionSubmissionUploadedOperation, - OperationSummary: "Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> Uploaded", - OperationID: "actionSubmissionUploaded", - Body: nil, - Params: middleware.Parameters{ - { - Name: "SubmissionID", - In: "path", - }: params.SubmissionID, - }, - Raw: r, - } - - type ( - Request = struct{} - Params = ActionSubmissionUploadedParams - Response = *ActionSubmissionUploadedNoContent - ) - response, err = middleware.HookMiddleware[ - Request, - Params, - Response, - ]( - m, - mreq, - unpackActionSubmissionUploadedParams, - func(ctx context.Context, request Request, params Params) (response Response, err error) { - err = s.h.ActionSubmissionUploaded(ctx, params) - return response, err - }, - ) - } else { - err = s.h.ActionSubmissionUploaded(ctx, params) - } - if err != nil { - if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { - if err := encodeErrorResponse(errRes, w, span); err != nil { - defer recordError("Internal", err) - } - return - } - if errors.Is(err, ht.ErrNotImplemented) { - s.cfg.ErrorHandler(ctx, w, r, err) - return - } - if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { - defer recordError("Internal", err) - } - return - } - - if err := encodeActionSubmissionUploadedResponse(response, w, span); err != nil { - defer recordError("EncodeResponse", err) - if !errors.Is(err, ht.ErrInternalServerErrorResponse) { - s.cfg.ErrorHandler(ctx, w, r, err) - } - return - } -} - // handleActionSubmissionValidatedRequest handles actionSubmissionValidated operation. // // Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> diff --git a/pkg/api/oas_operations_gen.go b/pkg/api/oas_operations_gen.go index ec1d73d..fa2d75d 100644 --- a/pkg/api/oas_operations_gen.go +++ b/pkg/api/oas_operations_gen.go @@ -29,7 +29,6 @@ const ( ActionSubmissionTriggerSubmitUncheckedOperation OperationName = "ActionSubmissionTriggerSubmitUnchecked" ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload" ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate" - ActionSubmissionUploadedOperation OperationName = "ActionSubmissionUploaded" ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated" CreateMapfixOperation OperationName = "CreateMapfix" CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment" diff --git a/pkg/api/oas_parameters_gen.go b/pkg/api/oas_parameters_gen.go index ba64fd2..c1fdcd0 100644 --- a/pkg/api/oas_parameters_gen.go +++ b/pkg/api/oas_parameters_gen.go @@ -1924,89 +1924,6 @@ func decodeActionSubmissionTriggerValidateParams(args [1]string, argsEscaped boo return params, nil } -// ActionSubmissionUploadedParams is parameters of actionSubmissionUploaded operation. -type ActionSubmissionUploadedParams struct { - // The unique identifier for a submission. - SubmissionID int64 -} - -func unpackActionSubmissionUploadedParams(packed middleware.Parameters) (params ActionSubmissionUploadedParams) { - { - key := middleware.ParameterKey{ - Name: "SubmissionID", - In: "path", - } - params.SubmissionID = packed[key].(int64) - } - return params -} - -func decodeActionSubmissionUploadedParams(args [1]string, argsEscaped bool, r *http.Request) (params ActionSubmissionUploadedParams, _ error) { - // Decode path: SubmissionID. - if err := func() error { - param := args[0] - if argsEscaped { - unescaped, err := url.PathUnescape(args[0]) - if err != nil { - return errors.Wrap(err, "unescape path") - } - param = unescaped - } - if len(param) > 0 { - d := uri.NewPathDecoder(uri.PathDecoderConfig{ - Param: "SubmissionID", - Value: param, - Style: uri.PathStyleSimple, - Explode: false, - }) - - if err := func() error { - val, err := d.DecodeValue() - if err != nil { - return err - } - - c, err := conv.ToInt64(val) - if err != nil { - return err - } - - params.SubmissionID = c - return nil - }(); err != nil { - return err - } - if err := func() error { - if err := (validate.Int{ - MinSet: true, - Min: 0, - MaxSet: false, - Max: 0, - MinExclusive: false, - MaxExclusive: false, - MultipleOfSet: false, - MultipleOf: 0, - }).Validate(int64(params.SubmissionID)); err != nil { - return errors.Wrap(err, "int") - } - return nil - }(); err != nil { - return err - } - } else { - return validate.ErrFieldRequired - } - return nil - }(); err != nil { - return params, &ogenerrors.DecodeParamError{ - Name: "SubmissionID", - In: "path", - Err: err, - } - } - return params, nil -} - // ActionSubmissionValidatedParams is parameters of actionSubmissionValidated operation. type ActionSubmissionValidatedParams struct { // The unique identifier for a submission. diff --git a/pkg/api/oas_response_decoders_gen.go b/pkg/api/oas_response_decoders_gen.go index 325fdeb..230e890 100644 --- a/pkg/api/oas_response_decoders_gen.go +++ b/pkg/api/oas_response_decoders_gen.go @@ -1396,66 +1396,6 @@ func decodeActionSubmissionTriggerValidateResponse(resp *http.Response) (res *Ac return res, errors.Wrap(defRes, "error") } -func decodeActionSubmissionUploadedResponse(resp *http.Response) (res *ActionSubmissionUploadedNoContent, _ error) { - switch resp.StatusCode { - case 204: - // Code 204. - return &ActionSubmissionUploadedNoContent{}, nil - } - // Convenient error response. - defRes, err := func() (res *ErrorStatusCode, err error) { - ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if err != nil { - return res, errors.Wrap(err, "parse media type") - } - switch { - case ct == "application/json": - buf, err := io.ReadAll(resp.Body) - if err != nil { - return res, err - } - d := jx.DecodeBytes(buf) - - var response Error - if err := func() error { - if err := response.Decode(d); err != nil { - return err - } - if err := d.Skip(); err != io.EOF { - return errors.New("unexpected trailing data") - } - return nil - }(); err != nil { - err = &ogenerrors.DecodeBodyError{ - ContentType: ct, - Body: buf, - Err: err, - } - return res, err - } - // Validate response. - if err := func() error { - if err := response.Validate(); err != nil { - return err - } - return nil - }(); err != nil { - return res, errors.Wrap(err, "validate") - } - return &ErrorStatusCode{ - StatusCode: resp.StatusCode, - Response: response, - }, nil - default: - return res, validate.InvalidContentType(ct) - } - }() - if err != nil { - return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) - } - return res, errors.Wrap(defRes, "error") -} - func decodeActionSubmissionValidatedResponse(resp *http.Response) (res *ActionSubmissionValidatedNoContent, _ error) { switch resp.StatusCode { case 204: diff --git a/pkg/api/oas_response_encoders_gen.go b/pkg/api/oas_response_encoders_gen.go index 14300be..ee6fc82 100644 --- a/pkg/api/oas_response_encoders_gen.go +++ b/pkg/api/oas_response_encoders_gen.go @@ -175,13 +175,6 @@ func encodeActionSubmissionTriggerValidateResponse(response *ActionSubmissionTri return nil } -func encodeActionSubmissionUploadedResponse(response *ActionSubmissionUploadedNoContent, w http.ResponseWriter, span trace.Span) error { - w.WriteHeader(204) - span.SetStatus(codes.Ok, http.StatusText(204)) - - return nil -} - func encodeActionSubmissionValidatedResponse(response *ActionSubmissionValidatedNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) diff --git a/pkg/api/oas_router_gen.go b/pkg/api/oas_router_gen.go index 2ea8902..50cc71f 100644 --- a/pkg/api/oas_router_gen.go +++ b/pkg/api/oas_router_gen.go @@ -1204,28 +1204,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { - case 'r': // Prefix: "releasing" - - if l := len("releasing"); len(elem) >= l && elem[0:l] == "releasing" { - elem = elem[l:] - } else { - break - } - - if len(elem) == 0 { - // Leaf node. - switch r.Method { - case "POST": - s.handleActionSubmissionUploadedRequest([1]string{ - args[0], - }, elemIsEscaped, w, r) - default: - s.notAllowed(w, r, "POST") - } - - return - } - case 's': // Prefix: "submitting" if l := len("submitting"); len(elem) >= l && elem[0:l] == "submitting" { @@ -2830,30 +2808,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { - case 'r': // Prefix: "releasing" - - if l := len("releasing"); len(elem) >= l && elem[0:l] == "releasing" { - elem = elem[l:] - } else { - break - } - - if len(elem) == 0 { - // Leaf node. - switch method { - case "POST": - r.name = ActionSubmissionUploadedOperation - r.summary = "Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> Uploaded" - r.operationID = "actionSubmissionUploaded" - r.pathPattern = "/submissions/{SubmissionID}/status/reset-releasing" - r.args = args - r.count = 1 - return r, true - default: - return - } - } - case 's': // Prefix: "submitting" if l := len("submitting"); len(elem) >= l && elem[0:l] == "submitting" { diff --git a/pkg/api/oas_schemas_gen.go b/pkg/api/oas_schemas_gen.go index 91c3ccb..8e8de02 100644 --- a/pkg/api/oas_schemas_gen.go +++ b/pkg/api/oas_schemas_gen.go @@ -83,9 +83,6 @@ type ActionSubmissionTriggerUploadNoContent struct{} // ActionSubmissionTriggerValidateNoContent is response for ActionSubmissionTriggerValidate operation. type ActionSubmissionTriggerValidateNoContent struct{} -// ActionSubmissionUploadedNoContent is response for ActionSubmissionUploaded operation. -type ActionSubmissionUploadedNoContent struct{} - // ActionSubmissionValidatedNoContent is response for ActionSubmissionValidated operation. type ActionSubmissionValidatedNoContent struct{} diff --git a/pkg/api/oas_security_gen.go b/pkg/api/oas_security_gen.go index 96068eb..ddc2925 100644 --- a/pkg/api/oas_security_gen.go +++ b/pkg/api/oas_security_gen.go @@ -57,7 +57,6 @@ var operationRolesCookieAuth = map[string][]string{ ActionSubmissionTriggerSubmitUncheckedOperation: []string{}, ActionSubmissionTriggerUploadOperation: []string{}, ActionSubmissionTriggerValidateOperation: []string{}, - ActionSubmissionUploadedOperation: []string{}, ActionSubmissionValidatedOperation: []string{}, CreateMapfixOperation: []string{}, CreateMapfixAuditCommentOperation: []string{}, diff --git a/pkg/api/oas_server_gen.go b/pkg/api/oas_server_gen.go index edde10a..d3ece17 100644 --- a/pkg/api/oas_server_gen.go +++ b/pkg/api/oas_server_gen.go @@ -148,13 +148,6 @@ type Handler interface { // // POST /submissions/{SubmissionID}/status/trigger-validate ActionSubmissionTriggerValidate(ctx context.Context, params ActionSubmissionTriggerValidateParams) error - // ActionSubmissionUploaded implements actionSubmissionUploaded operation. - // - // Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> - // Uploaded. - // - // POST /submissions/{SubmissionID}/status/reset-releasing - ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error // ActionSubmissionValidated implements actionSubmissionValidated operation. // // Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> diff --git a/pkg/api/oas_unimplemented_gen.go b/pkg/api/oas_unimplemented_gen.go index 6f9106c..1069f43 100644 --- a/pkg/api/oas_unimplemented_gen.go +++ b/pkg/api/oas_unimplemented_gen.go @@ -222,16 +222,6 @@ func (UnimplementedHandler) ActionSubmissionTriggerValidate(ctx context.Context, return ht.ErrNotImplemented } -// ActionSubmissionUploaded implements actionSubmissionUploaded operation. -// -// Role SubmissionRelease manually resets releasing softlock and changes status from Releasing -> -// Uploaded. -// -// POST /submissions/{SubmissionID}/status/reset-releasing -func (UnimplementedHandler) ActionSubmissionUploaded(ctx context.Context, params ActionSubmissionUploadedParams) error { - return ht.ErrNotImplemented -} - // ActionSubmissionValidated implements actionSubmissionValidated operation. // // Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> From efd60f45df7de2d885f4a4d8766cb4fa5ce1b6f6 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 14 Aug 2025 18:42:55 -0700 Subject: [PATCH 15/27] validator: respond correctly to upload failure --- validation/src/upload_mapfix.rs | 90 +++++++++++++++++++++------- validation/src/upload_submission.rs | 91 ++++++++++++++++++++++------- 2 files changed, 139 insertions(+), 42 deletions(-) diff --git a/validation/src/upload_mapfix.rs b/validation/src/upload_mapfix.rs index e943922..17cdca4 100644 --- a/validation/src/upload_mapfix.rs +++ b/validation/src/upload_mapfix.rs @@ -3,11 +3,49 @@ use crate::nats_types::UploadMapfixRequest; #[allow(dead_code)] #[derive(Debug)] -pub enum Error{ +pub enum InnerError{ Download(crate::download::Error), IO(std::io::Error), Json(serde_json::Error), Upload(rbx_asset::cookie::UploadError), +} +impl std::fmt::Display for InnerError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for InnerError{} + +async fn upload_inner( + upload_info:UploadMapfixRequest, + cloud_context:&rbx_asset::cloud::Context, + cookie_context:&rbx_asset::cookie::Context, + group_id:Option, +)->Result<(),InnerError>{ + // download the map model + let maybe_gzip=download_asset_version(cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ + asset_id:upload_info.ModelID, + version:upload_info.ModelVersion, + }).await.map_err(InnerError::Download)?; + + // transparently handle gzipped models + let model_data=maybe_gzip.to_vec().map_err(InnerError::IO)?; + + // upload the map to the strafesnet group + let _upload_response=cookie_context.upload(rbx_asset::cookie::UploadRequest{ + assetid:upload_info.TargetAssetID, + groupId:group_id, + name:None, + description:None, + ispublic:None, + allowComments:None, + },model_data).await.map_err(InnerError::Upload)?; + + Ok(()) +} +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ ApiActionMapfixUploaded(tonic::Status), } impl std::fmt::Display for Error{ @@ -19,29 +57,39 @@ impl std::error::Error for Error{} impl crate::message_handler::MessageHandler{ pub async fn upload_mapfix(&self,upload_info:UploadMapfixRequest)->Result<(),Error>{ - // download the map model - let maybe_gzip=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ - asset_id:upload_info.ModelID, - version:upload_info.ModelVersion, - }).await.map_err(Error::Download)?; + let mapfix_id=upload_info.MapfixID; + let result=upload_inner( + upload_info, + &self.cloud_context, + &self.cookie_context, + self.group_id, + ).await; - // transparently handle gzipped models - let model_data=maybe_gzip.to_vec().map_err(Error::IO)?; + // update the mapfix depending on the result + match result{ + Ok(())=>{ + // mark mapfix as uploaded, TargetAssetID is unchanged + self.mapfixes.set_status_uploaded(rust_grpc::validator::MapfixId{ + id:mapfix_id, + }).await.map_err(Error::ApiActionMapfixUploaded)?; + }, + Err(e)=>{ + // log error + println!("[upload_mapfix] Error: {e}"); - // upload the map to the strafesnet group - let _upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{ - assetid:upload_info.TargetAssetID, - groupId:self.group_id, - name:None, - description:None, - ispublic:None, - allowComments:None, - },model_data).await.map_err(Error::Upload)?; + self.mapfixes.create_audit_error( + rust_grpc::validator::AuditErrorRequest{ + id:mapfix_id, + error_message:e.to_string(), + } + ).await.map_err(Error::ApiActionMapfixUploaded)?; - // mark mapfix as uploaded, TargetAssetID is unchanged - self.mapfixes.set_status_uploaded(rust_grpc::validator::MapfixId{ - id:upload_info.MapfixID, - }).await.map_err(Error::ApiActionMapfixUploaded)?; + // update the mapfix model status to accepted + self.mapfixes.set_status_not_uploaded(rust_grpc::validator::MapfixId{ + id:mapfix_id, + }).await.map_err(Error::ApiActionMapfixUploaded)?; + }, + } Ok(()) } diff --git a/validation/src/upload_submission.rs b/validation/src/upload_submission.rs index 3b6319d..abd762b 100644 --- a/validation/src/upload_submission.rs +++ b/validation/src/upload_submission.rs @@ -3,12 +3,50 @@ use crate::nats_types::UploadSubmissionRequest; #[allow(dead_code)] #[derive(Debug)] -pub enum Error{ +pub enum InnerError{ Download(crate::download::Error), IO(std::io::Error), Json(serde_json::Error), Create(rbx_asset::cookie::CreateError), SystemTime(std::time::SystemTimeError), +} +impl std::fmt::Display for InnerError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for InnerError{} + +async fn upload_inner( + upload_info:UploadSubmissionRequest, + cloud_context:&rbx_asset::cloud::Context, + cookie_context:&rbx_asset::cookie::Context, + group_id:Option, +)->Result{ + // download the map model + let maybe_gzip=download_asset_version(cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ + asset_id:upload_info.ModelID, + version:upload_info.ModelVersion, + }).await.map_err(InnerError::Download)?; + + // transparently handle gzipped models + let model_data=maybe_gzip.to_vec().map_err(InnerError::IO)?; + + // upload the map to the strafesnet group + let upload_response=cookie_context.create(rbx_asset::cookie::CreateRequest{ + name:upload_info.ModelName.clone(), + description:"".to_owned(), + ispublic:false, + allowComments:false, + groupId:group_id, + },model_data).await.map_err(InnerError::Create)?; + + Ok(upload_response.AssetId) +} + +#[allow(dead_code)] +#[derive(Debug)] +pub enum Error{ ApiActionSubmissionUploaded(tonic::Status), } impl std::fmt::Display for Error{ @@ -20,29 +58,40 @@ impl std::error::Error for Error{} impl crate::message_handler::MessageHandler{ pub async fn upload_submission(&self,upload_info:UploadSubmissionRequest)->Result<(),Error>{ - // download the map model - let maybe_gzip=download_asset_version(&self.cloud_context,rbx_asset::cloud::GetAssetVersionRequest{ - asset_id:upload_info.ModelID, - version:upload_info.ModelVersion, - }).await.map_err(Error::Download)?; + let submission_id=upload_info.SubmissionID; + let result=upload_inner( + upload_info, + &self.cloud_context, + &self.cookie_context, + self.group_id, + ).await; - // transparently handle gzipped models - let model_data=maybe_gzip.to_vec().map_err(Error::IO)?; + // update the submission depending on the result + match result{ + Ok(uploaded_asset_id)=>{ + // note the asset id of the created model for later release, and mark the submission as uploaded + self.submissions.set_status_uploaded(rust_grpc::validator::StatusUploadedRequest{ + id:submission_id, + uploaded_asset_id, + }).await.map_err(Error::ApiActionSubmissionUploaded)?; + }, + Err(e)=>{ + // log error + println!("[upload_submission] Error: {e}"); - // upload the map to the strafesnet group - let upload_response=self.cookie_context.create(rbx_asset::cookie::CreateRequest{ - name:upload_info.ModelName.clone(), - description:"".to_owned(), - ispublic:false, - allowComments:false, - groupId:self.group_id, - },model_data).await.map_err(Error::Create)?; + self.submissions.create_audit_error( + rust_grpc::validator::AuditErrorRequest{ + id:submission_id, + error_message:e.to_string(), + } + ).await.map_err(Error::ApiActionSubmissionUploaded)?; - // note the asset id of the created model for later release, and mark the submission as uploaded - self.submissions.set_status_uploaded(rust_grpc::validator::StatusUploadedRequest{ - id:upload_info.SubmissionID, - uploaded_asset_id:upload_response.AssetId, - }).await.map_err(Error::ApiActionSubmissionUploaded)?; + // update the submission model status to accepted + self.submissions.set_status_not_uploaded(rust_grpc::validator::SubmissionId{ + id:submission_id, + }).await.map_err(Error::ApiActionSubmissionUploaded)?; + }, + } Ok(()) } From b5b07ec1ce45a231ad197a4dc58d04f48bb0713c Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 17:13:59 -0700 Subject: [PATCH 16/27] openapi: create migration endpoint --- openapi.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index e02f122..d11507e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -312,6 +312,21 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /mapfixes/migrate: + post: + summary: Perform the Uploaded -> Released migration. + operationId: migrateMapfixes + tags: + - Mapfixes + responses: + "204": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /mapfixes/{MapfixID}: get: summary: Retrieve map with ID From 1a558f35cf268569a021674db61f82e1826d0188 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 17:14:05 -0700 Subject: [PATCH 17/27] openapi: generate --- pkg/api/oas_client_gen.go | 111 +++++++++++++++++ pkg/api/oas_handlers_gen.go | 180 +++++++++++++++++++++++++++ pkg/api/oas_operations_gen.go | 1 + pkg/api/oas_response_decoders_gen.go | 60 +++++++++ pkg/api/oas_response_encoders_gen.go | 7 ++ pkg/api/oas_router_gen.go | 56 +++++++++ pkg/api/oas_schemas_gen.go | 3 + pkg/api/oas_security_gen.go | 1 + pkg/api/oas_server_gen.go | 6 + pkg/api/oas_unimplemented_gen.go | 9 ++ 10 files changed, 434 insertions(+) diff --git a/pkg/api/oas_client_gen.go b/pkg/api/oas_client_gen.go index 7f760bd..922d386 100644 --- a/pkg/api/oas_client_gen.go +++ b/pkg/api/oas_client_gen.go @@ -314,6 +314,12 @@ type Invoker interface { // // GET /submissions ListSubmissions(ctx context.Context, params ListSubmissionsParams) (*Submissions, error) + // MigrateMapfixes invokes migrateMapfixes operation. + // + // Perform the Uploaded -> Released migration. + // + // POST /mapfixes/migrate + MigrateMapfixes(ctx context.Context) error // ReleaseSubmissions invokes releaseSubmissions operation. // // Release a set of uploaded maps. Role SubmissionRelease. @@ -6383,6 +6389,111 @@ func (c *Client) sendListSubmissions(ctx context.Context, params ListSubmissions return result, nil } +// MigrateMapfixes invokes migrateMapfixes operation. +// +// Perform the Uploaded -> Released migration. +// +// POST /mapfixes/migrate +func (c *Client) MigrateMapfixes(ctx context.Context) error { + _, err := c.sendMigrateMapfixes(ctx) + return err +} + +func (c *Client) sendMigrateMapfixes(ctx context.Context) (res *MigrateMapfixesNoContent, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("migrateMapfixes"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/migrate"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, MigrateMapfixesOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/mapfixes/migrate" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:CookieAuth" + switch err := c.securityCookieAuth(ctx, MigrateMapfixesOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"CookieAuth\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeMigrateMapfixesResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // ReleaseSubmissions invokes releaseSubmissions operation. // // Release a set of uploaded maps. Role SubmissionRelease. diff --git a/pkg/api/oas_handlers_gen.go b/pkg/api/oas_handlers_gen.go index d1d2d6e..6812a6e 100644 --- a/pkg/api/oas_handlers_gen.go +++ b/pkg/api/oas_handlers_gen.go @@ -8824,6 +8824,186 @@ func (s *Server) handleListSubmissionsRequest(args [0]string, argsEscaped bool, } } +// handleMigrateMapfixesRequest handles migrateMapfixes operation. +// +// Perform the Uploaded -> Released migration. +// +// POST /mapfixes/migrate +func (s *Server) handleMigrateMapfixesRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("migrateMapfixes"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/mapfixes/migrate"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), MigrateMapfixesOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code >= 100 && code < 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: MigrateMapfixesOperation, + ID: "migrateMapfixes", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityCookieAuth(ctx, MigrateMapfixesOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "CookieAuth", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security:CookieAuth", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + + var response *MigrateMapfixesNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: MigrateMapfixesOperation, + OperationSummary: "Perform the Uploaded -> Released migration.", + OperationID: "migrateMapfixes", + Body: nil, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = *MigrateMapfixesNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.MigrateMapfixes(ctx) + return response, err + }, + ) + } else { + err = s.h.MigrateMapfixes(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeMigrateMapfixesResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleReleaseSubmissionsRequest handles releaseSubmissions operation. // // Release a set of uploaded maps. Role SubmissionRelease. diff --git a/pkg/api/oas_operations_gen.go b/pkg/api/oas_operations_gen.go index fa2d75d..dcd30ac 100644 --- a/pkg/api/oas_operations_gen.go +++ b/pkg/api/oas_operations_gen.go @@ -53,6 +53,7 @@ const ( ListScriptsOperation OperationName = "ListScripts" ListSubmissionAuditEventsOperation OperationName = "ListSubmissionAuditEvents" ListSubmissionsOperation OperationName = "ListSubmissions" + MigrateMapfixesOperation OperationName = "MigrateMapfixes" ReleaseSubmissionsOperation OperationName = "ReleaseSubmissions" SessionRolesOperation OperationName = "SessionRoles" SessionUserOperation OperationName = "SessionUser" diff --git a/pkg/api/oas_response_decoders_gen.go b/pkg/api/oas_response_decoders_gen.go index 230e890..c4e0e14 100644 --- a/pkg/api/oas_response_decoders_gen.go +++ b/pkg/api/oas_response_decoders_gen.go @@ -3715,6 +3715,66 @@ func decodeListSubmissionsResponse(resp *http.Response) (res *Submissions, _ err return res, errors.Wrap(defRes, "error") } +func decodeMigrateMapfixesResponse(resp *http.Response) (res *MigrateMapfixesNoContent, _ error) { + switch resp.StatusCode { + case 204: + // Code 204. + return &MigrateMapfixesNoContent{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeReleaseSubmissionsResponse(resp *http.Response) (res *ReleaseSubmissionsCreated, _ error) { switch resp.StatusCode { case 201: diff --git a/pkg/api/oas_response_encoders_gen.go b/pkg/api/oas_response_encoders_gen.go index ee6fc82..85976a5 100644 --- a/pkg/api/oas_response_encoders_gen.go +++ b/pkg/api/oas_response_encoders_gen.go @@ -498,6 +498,13 @@ func encodeListSubmissionsResponse(response *Submissions, w http.ResponseWriter, return nil } +func encodeMigrateMapfixesResponse(response *MigrateMapfixesNoContent, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(204) + span.SetStatus(codes.Ok, http.StatusText(204)) + + return nil +} + func encodeReleaseSubmissionsResponse(response *ReleaseSubmissionsCreated, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(201) span.SetStatus(codes.Ok, http.StatusText(201)) diff --git a/pkg/api/oas_router_gen.go b/pkg/api/oas_router_gen.go index 50cc71f..dce160b 100644 --- a/pkg/api/oas_router_gen.go +++ b/pkg/api/oas_router_gen.go @@ -102,6 +102,32 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } + if len(elem) == 0 { + break + } + switch elem[0] { + case 'm': // Prefix: "migrate" + origElem := elem + if l := len("migrate"); len(elem) >= l && elem[0:l] == "migrate" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleMigrateMapfixesRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + + elem = origElem + } // Param: "MapfixID" // Match until "/" idx := strings.IndexByte(elem, '/') @@ -1576,6 +1602,36 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } + if len(elem) == 0 { + break + } + switch elem[0] { + case 'm': // Prefix: "migrate" + origElem := elem + if l := len("migrate"); len(elem) >= l && elem[0:l] == "migrate" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = MigrateMapfixesOperation + r.summary = "Perform the Uploaded -> Released migration." + r.operationID = "migrateMapfixes" + r.pathPattern = "/mapfixes/migrate" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + elem = origElem + } // Param: "MapfixID" // Match until "/" idx := strings.IndexByte(elem, '/') diff --git a/pkg/api/oas_schemas_gen.go b/pkg/api/oas_schemas_gen.go index 8e8de02..13fe069 100644 --- a/pkg/api/oas_schemas_gen.go +++ b/pkg/api/oas_schemas_gen.go @@ -670,6 +670,9 @@ func (s *Mapfixes) SetMapfixes(val []Mapfix) { s.Mapfixes = val } +// MigrateMapfixesNoContent is response for MigrateMapfixes operation. +type MigrateMapfixesNoContent struct{} + // Ref: #/components/schemas/Operation type Operation struct { OperationID int32 `json:"OperationID"` diff --git a/pkg/api/oas_security_gen.go b/pkg/api/oas_security_gen.go index ddc2925..ad72e8e 100644 --- a/pkg/api/oas_security_gen.go +++ b/pkg/api/oas_security_gen.go @@ -69,6 +69,7 @@ var operationRolesCookieAuth = map[string][]string{ DeleteScriptPolicyOperation: []string{}, DownloadMapAssetOperation: []string{}, GetOperationOperation: []string{}, + MigrateMapfixesOperation: []string{}, ReleaseSubmissionsOperation: []string{}, SessionRolesOperation: []string{}, SessionUserOperation: []string{}, diff --git a/pkg/api/oas_server_gen.go b/pkg/api/oas_server_gen.go index d3ece17..53b80df 100644 --- a/pkg/api/oas_server_gen.go +++ b/pkg/api/oas_server_gen.go @@ -293,6 +293,12 @@ type Handler interface { // // GET /submissions ListSubmissions(ctx context.Context, params ListSubmissionsParams) (*Submissions, error) + // MigrateMapfixes implements migrateMapfixes operation. + // + // Perform the Uploaded -> Released migration. + // + // POST /mapfixes/migrate + MigrateMapfixes(ctx context.Context) error // ReleaseSubmissions implements releaseSubmissions operation. // // Release a set of uploaded maps. Role SubmissionRelease. diff --git a/pkg/api/oas_unimplemented_gen.go b/pkg/api/oas_unimplemented_gen.go index 1069f43..63f35e7 100644 --- a/pkg/api/oas_unimplemented_gen.go +++ b/pkg/api/oas_unimplemented_gen.go @@ -439,6 +439,15 @@ func (UnimplementedHandler) ListSubmissions(ctx context.Context, params ListSubm return r, ht.ErrNotImplemented } +// MigrateMapfixes implements migrateMapfixes operation. +// +// Perform the Uploaded -> Released migration. +// +// POST /mapfixes/migrate +func (UnimplementedHandler) MigrateMapfixes(ctx context.Context) error { + return ht.ErrNotImplemented +} + // ReleaseSubmissions implements releaseSubmissions operation. // // Release a set of uploaded maps. Role SubmissionRelease. From d7456d500bd1afdf6d7723c0b0acfd51c2479623 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 17:48:45 -0700 Subject: [PATCH 18/27] backend: create mapfixes migration code UPLOADED -> RELEASED --- pkg/datastore/datastore.go | 1 + pkg/datastore/gormstore/mapfixes.go | 17 +++++++++++++++++ pkg/service/mapfixes.go | 4 ++++ pkg/web_api/mapfixes.go | 9 +++++++++ 4 files changed, 31 insertions(+) diff --git a/pkg/datastore/datastore.go b/pkg/datastore/datastore.go index 0c43d1c..5d4a381 100644 --- a/pkg/datastore/datastore.go +++ b/pkg/datastore/datastore.go @@ -61,6 +61,7 @@ type Mapfixes interface { Delete(ctx context.Context, id int64) error List(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort) ([]model.Mapfix, error) ListWithTotal(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort) (int64, []model.Mapfix, error) + Migrate(ctx context.Context) error } type Operations interface { diff --git a/pkg/datastore/gormstore/mapfixes.go b/pkg/datastore/gormstore/mapfixes.go index ee4f0ad..af59fa4 100644 --- a/pkg/datastore/gormstore/mapfixes.go +++ b/pkg/datastore/gormstore/mapfixes.go @@ -8,6 +8,8 @@ import ( "git.itzana.me/strafesnet/maps-service/pkg/model" "gorm.io/gorm" "gorm.io/gorm/clause" + + log "github.com/sirupsen/logrus" ) type Mapfixes struct { @@ -151,3 +153,18 @@ func (env *Mapfixes) ListWithTotal(ctx context.Context, filters datastore.Option return total, maps, nil } + +func (env *Mapfixes) Migrate(ctx context.Context) error { + migrate_from := model.MapfixStatusUploaded + migrate_to := model.MapfixStatusReleased + count := int64(0) + err := env.db. + Model(&model.Mapfix{}). + Where("status_id = ?", migrate_from). + Set("status_id = ?", migrate_to). + Count(&count). + Error + + log.Println("Mafixes Migration rows affected:",count) + return err +} diff --git a/pkg/service/mapfixes.go b/pkg/service/mapfixes.go index 9ca7fe4..88f81f4 100644 --- a/pkg/service/mapfixes.go +++ b/pkg/service/mapfixes.go @@ -114,3 +114,7 @@ func (svc *Service) UpdateMapfixIfStatus(ctx context.Context, id int64, statuses func (svc *Service) UpdateAndGetMapfixIfStatus(ctx context.Context, id int64, statuses []model.MapfixStatus, pmap MapfixUpdate) (model.Mapfix, error) { return svc.db.Mapfixes().IfStatusThenUpdateAndGet(ctx, id, statuses, datastore.OptionalMap(pmap)) } + +func (svc *Service) MigrateMapfixes(ctx context.Context) error { + return svc.db.Mapfixes().Migrate(ctx) +} diff --git a/pkg/web_api/mapfixes.go b/pkg/web_api/mapfixes.go index b72d415..cd718e8 100644 --- a/pkg/web_api/mapfixes.go +++ b/pkg/web_api/mapfixes.go @@ -1189,3 +1189,12 @@ func (svc *Service) ListMapfixAuditEvents(ctx context.Context, params api.ListMa }, ) } + +// MigrateMapfixes invokes MigrateMapfixes operation. +// +// Retrieve a list of audit events. +// +// GET /mapfixes/{MapfixID}/audit-events +func (svc *Service) MigrateMapfixes(ctx context.Context) error { + return svc.inner.MigrateMapfixes(ctx) +} From de6163093f32630b241a460cac5251da121a2a1d Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 18:38:34 -0700 Subject: [PATCH 19/27] backend: add missing list query --- openapi.yaml | 12 +++ pkg/api/oas_client_gen.go | 34 +++++++ pkg/api/oas_handlers_gen.go | 8 ++ pkg/api/oas_parameters_gen.go | 150 +++++++++++++++++++++++++++++ pkg/web_api/mapfixes.go | 3 + pkg/web_api/submissions.go | 3 + submissions-api-rs/src/external.rs | 3 + submissions-api-rs/src/types.rs | 1 + 8 files changed, 214 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index d11507e..0c8faf5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -244,6 +244,12 @@ paths: type: integer format: int64 minimum: 0 + - name: AssetVersion + in: query + schema: + type: integer + format: int64 + minimum: 0 - name: TargetAssetID in: query schema: @@ -747,6 +753,12 @@ paths: type: integer format: int64 minimum: 0 + - name: AssetVersion + in: query + schema: + type: integer + format: int64 + minimum: 0 - name: UploadedAssetID in: query schema: diff --git a/pkg/api/oas_client_gen.go b/pkg/api/oas_client_gen.go index 922d386..361a9c1 100644 --- a/pkg/api/oas_client_gen.go +++ b/pkg/api/oas_client_gen.go @@ -5452,6 +5452,23 @@ func (c *Client) sendListMapfixes(ctx context.Context, params ListMapfixesParams return res, errors.Wrap(err, "encode query") } } + { + // Encode "AssetVersion" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "AssetVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.AssetVersion.Get(); ok { + return e.EncodeValue(conv.Int64ToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } { // Encode "TargetAssetID" parameter. cfg := uri.QueryParameterEncodingConfig{ @@ -6331,6 +6348,23 @@ func (c *Client) sendListSubmissions(ctx context.Context, params ListSubmissions return res, errors.Wrap(err, "encode query") } } + { + // Encode "AssetVersion" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "AssetVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.AssetVersion.Get(); ok { + return e.EncodeValue(conv.Int64ToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } { // Encode "UploadedAssetID" parameter. cfg := uri.QueryParameterEncodingConfig{ diff --git a/pkg/api/oas_handlers_gen.go b/pkg/api/oas_handlers_gen.go index 6812a6e..837522f 100644 --- a/pkg/api/oas_handlers_gen.go +++ b/pkg/api/oas_handlers_gen.go @@ -7916,6 +7916,10 @@ func (s *Server) handleListMapfixesRequest(args [0]string, argsEscaped bool, w h Name: "AssetID", In: "query", }: params.AssetID, + { + Name: "AssetVersion", + In: "query", + }: params.AssetVersion, { Name: "TargetAssetID", In: "query", @@ -8765,6 +8769,10 @@ func (s *Server) handleListSubmissionsRequest(args [0]string, argsEscaped bool, Name: "AssetID", In: "query", }: params.AssetID, + { + Name: "AssetVersion", + In: "query", + }: params.AssetVersion, { Name: "UploadedAssetID", In: "query", diff --git a/pkg/api/oas_parameters_gen.go b/pkg/api/oas_parameters_gen.go index c1fdcd0..5107683 100644 --- a/pkg/api/oas_parameters_gen.go +++ b/pkg/api/oas_parameters_gen.go @@ -3138,6 +3138,7 @@ type ListMapfixesParams struct { Sort OptInt32 Submitter OptInt64 AssetID OptInt64 + AssetVersion OptInt64 TargetAssetID OptInt64 // // Phase: Creation * `0` - UnderConstruction * `1` - ChangesRequested // // Phase: Review * `2` - Submitting * `3` - Submitted @@ -3217,6 +3218,15 @@ func unpackListMapfixesParams(packed middleware.Parameters) (params ListMapfixes params.AssetID = v.(OptInt64) } } + { + key := middleware.ParameterKey{ + Name: "AssetVersion", + In: "query", + } + if v, ok := packed[key]; ok { + params.AssetVersion = v.(OptInt64) + } + } { key := middleware.ParameterKey{ Name: "TargetAssetID", @@ -3734,6 +3744,71 @@ func decodeListMapfixesParams(args [0]string, argsEscaped bool, r *http.Request) Err: err, } } + // Decode query: AssetVersion. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "AssetVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotAssetVersionVal int64 + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + paramsDotAssetVersionVal = c + return nil + }(); err != nil { + return err + } + params.AssetVersion.SetTo(paramsDotAssetVersionVal) + return nil + }); err != nil { + return err + } + if err := func() error { + if value, ok := params.AssetVersion.Get(); ok { + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(value)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "AssetVersion", + In: "query", + Err: err, + } + } // Decode query: TargetAssetID. if err := func() error { cfg := uri.QueryParameterDecodingConfig{ @@ -5387,6 +5462,7 @@ type ListSubmissionsParams struct { Sort OptInt32 Submitter OptInt64 AssetID OptInt64 + AssetVersion OptInt64 UploadedAssetID OptInt64 // // Phase: Creation * `0` - UnderConstruction * `1` - ChangesRequested // // Phase: Review * `2` - Submitting * `3` - Submitted @@ -5466,6 +5542,15 @@ func unpackListSubmissionsParams(packed middleware.Parameters) (params ListSubmi params.AssetID = v.(OptInt64) } } + { + key := middleware.ParameterKey{ + Name: "AssetVersion", + In: "query", + } + if v, ok := packed[key]; ok { + params.AssetVersion = v.(OptInt64) + } + } { key := middleware.ParameterKey{ Name: "UploadedAssetID", @@ -5983,6 +6068,71 @@ func decodeListSubmissionsParams(args [0]string, argsEscaped bool, r *http.Reque Err: err, } } + // Decode query: AssetVersion. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "AssetVersion", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotAssetVersionVal int64 + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + paramsDotAssetVersionVal = c + return nil + }(); err != nil { + return err + } + params.AssetVersion.SetTo(paramsDotAssetVersionVal) + return nil + }); err != nil { + return err + } + if err := func() error { + if value, ok := params.AssetVersion.Get(); ok { + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(value)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "AssetVersion", + In: "query", + Err: err, + } + } // Decode query: UploadedAssetID. if err := func() error { cfg := uri.QueryParameterDecodingConfig{ diff --git a/pkg/web_api/mapfixes.go b/pkg/web_api/mapfixes.go index cd718e8..245d257 100644 --- a/pkg/web_api/mapfixes.go +++ b/pkg/web_api/mapfixes.go @@ -195,6 +195,9 @@ func (svc *Service) ListMapfixes(ctx context.Context, params api.ListMapfixesPar 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 target_asset_id, target_asset_id_ok := params.TargetAssetID.Get(); target_asset_id_ok{ filter.SetTargetAssetID(uint64(target_asset_id)) } diff --git a/pkg/web_api/submissions.go b/pkg/web_api/submissions.go index 5eed163..ef8bdd7 100644 --- a/pkg/web_api/submissions.go +++ b/pkg/web_api/submissions.go @@ -229,6 +229,9 @@ func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissi 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)) } diff --git a/submissions-api-rs/src/external.rs b/submissions-api-rs/src/external.rs index e32655c..0979faf 100644 --- a/submissions-api-rs/src/external.rs +++ b/submissions-api-rs/src/external.rs @@ -178,6 +178,9 @@ impl Context{ if let Some(asset_id)=config.AssetID{ query_pairs.append_pair("AssetID",asset_id.to_string().as_str()); } + if let Some(asset_version)=config.AssetVersion{ + query_pairs.append_pair("AssetVersion",asset_version.to_string().as_str()); + } if let Some(uploaded_asset_id)=config.UploadedAssetID{ query_pairs.append_pair("UploadedAssetID",uploaded_asset_id.to_string().as_str()); } diff --git a/submissions-api-rs/src/types.rs b/submissions-api-rs/src/types.rs index 98a08cc..f4dbc38 100644 --- a/submissions-api-rs/src/types.rs +++ b/submissions-api-rs/src/types.rs @@ -286,6 +286,7 @@ pub struct GetSubmissionsRequest<'a>{ pub GameID:Option, pub Submitter:Option, pub AssetID:Option, + pub AssetVersion:Option, pub UploadedAssetID:Option, pub StatusID:Option, } From 6ee8816eed5b6bcf942a614c7d7bae348ec71aaa Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 18:39:40 -0700 Subject: [PATCH 20/27] submissions-api-rs: add get_mapfixes endpoint --- submissions-api-rs/src/external.rs | 42 +++++++++++++++++++ submissions-api-rs/src/types.rs | 67 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/submissions-api-rs/src/external.rs b/submissions-api-rs/src/external.rs index 0979faf..fd540ae 100644 --- a/submissions-api-rs/src/external.rs +++ b/submissions-api-rs/src/external.rs @@ -152,6 +152,48 @@ impl Context{ Ok(()) } + pub async fn get_mapfixes(&self,config:GetMapfixesRequest<'_>)->Result{ + let url_raw=format!("{}/mapfixes",self.0.base_url); + let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?; + + { + let mut query_pairs=url.query_pairs_mut(); + query_pairs.append_pair("Page",config.Page.to_string().as_str()); + query_pairs.append_pair("Limit",config.Limit.to_string().as_str()); + if let Some(sort)=config.Sort{ + query_pairs.append_pair("Sort",(sort as u8).to_string().as_str()); + } + if let Some(display_name)=config.DisplayName{ + query_pairs.append_pair("DisplayName",display_name); + } + if let Some(creator)=config.Creator{ + query_pairs.append_pair("Creator",creator); + } + if let Some(game_id)=config.GameID{ + query_pairs.append_pair("GameID",(game_id as u8).to_string().as_str()); + } + if let Some(submitter)=config.Submitter{ + query_pairs.append_pair("Submitter",submitter.to_string().as_str()); + } + if let Some(asset_id)=config.AssetID{ + query_pairs.append_pair("AssetID",asset_id.to_string().as_str()); + } + if let Some(asset_version)=config.AssetVersion{ + query_pairs.append_pair("AssetVersion",asset_version.to_string().as_str()); + } + if let Some(uploaded_asset_id)=config.TargetAssetID{ + query_pairs.append_pair("TargetAssetID",uploaded_asset_id.to_string().as_str()); + } + if let Some(status_id)=config.StatusID{ + query_pairs.append_pair("StatusID",(status_id as u8).to_string().as_str()); + } + } + + response_ok( + self.0.get(url).await.map_err(Error::Reqwest)? + ).await.map_err(Error::Response)? + .json().await.map_err(Error::ReqwestJson) + } pub async fn get_submissions(&self,config:GetSubmissionsRequest<'_>)->Result{ let url_raw=format!("{}/submissions",self.0.base_url); let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?; diff --git a/submissions-api-rs/src/types.rs b/submissions-api-rs/src/types.rs index f4dbc38..5eef392 100644 --- a/submissions-api-rs/src/types.rs +++ b/submissions-api-rs/src/types.rs @@ -252,6 +252,73 @@ pub enum Sort{ DateDescending=4, } +#[derive(Clone,Debug,serde_repr::Serialize_repr,serde_repr::Deserialize_repr)] +#[repr(u8)] +pub enum MapfixStatus{ + // Phase: Creation + UnderConstruction=0, + ChangesRequested=1, + + // Phase: Review + Submitting=2, + Submitted=3, + + // Phase: Testing + AcceptedUnvalidated=4, // pending script review, can re-trigger validation + Validating=5, + Validated=6, + Uploading=7, + Uploaded=8, // uploaded to the group, but pending release + Releasing=11, + + // Phase: Final MapfixStatus + Rejected=9, + Released=10, +} + +#[allow(nonstandard_style)] +#[derive(Clone,Debug)] +pub struct GetMapfixesRequest<'a>{ + pub Page:u32, + pub Limit:u32, + pub Sort:Option, + pub DisplayName:Option<&'a str>, + pub Creator:Option<&'a str>, + pub GameID:Option, + pub Submitter:Option, + pub AssetID:Option, + pub AssetVersion:Option, + pub TargetAssetID:Option, + pub StatusID:Option, +} + +#[allow(nonstandard_style)] +#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)] +pub struct MapfixResponse{ + pub ID:MapfixID, + pub DisplayName:String, + pub Creator:String, + pub GameID:u32, + pub CreatedAt:i64, + pub UpdatedAt:i64, + pub Submitter:u64, + pub AssetID:u64, + pub AssetVersion:u64, + pub ValidatedAssetID:u64, + pub ValidatedAssetVersion:u64, + pub Completed:bool, + pub TargetAssetID:u64, + pub StatusID:MapfixStatus, + pub Description:String, +} + +#[allow(nonstandard_style)] +#[derive(Clone,Debug,serde::Deserialize)] +pub struct MapfixesResponse{ + pub Total:u64, + pub Mapfixes:Vec, +} + #[derive(Clone,Debug,serde_repr::Deserialize_repr)] #[repr(u8)] pub enum SubmissionStatus{ From 18ca6de7d3726e8c36da68794278412dbd68fe55 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 18:41:04 -0700 Subject: [PATCH 21/27] submissions-api-rs: v0.9.0 get_mapfixes --- Cargo.lock | 2 +- submissions-api-rs/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8dfc141..3b3d680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2057,7 +2057,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "submissions-api" -version = "0.8.2" +version = "0.9.0" dependencies = [ "chrono", "reqwest", diff --git a/submissions-api-rs/Cargo.toml b/submissions-api-rs/Cargo.toml index 9982ecf..6252bf3 100644 --- a/submissions-api-rs/Cargo.toml +++ b/submissions-api-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "submissions-api" -version = "0.8.2" +version = "0.9.0" edition = "2024" publish = ["strafesnet"] repository = "https://git.itzana.me/StrafesNET/maps-service" From 877f5c024fb2d3325aecd74b3d76f36c07e78bdc Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 19:01:11 -0700 Subject: [PATCH 22/27] submissions-api-rs: fix MapfixResponse --- submissions-api-rs/src/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submissions-api-rs/src/types.rs b/submissions-api-rs/src/types.rs index 5eef392..ff3bf67 100644 --- a/submissions-api-rs/src/types.rs +++ b/submissions-api-rs/src/types.rs @@ -304,8 +304,8 @@ pub struct MapfixResponse{ pub Submitter:u64, pub AssetID:u64, pub AssetVersion:u64, - pub ValidatedAssetID:u64, - pub ValidatedAssetVersion:u64, + pub ValidatedAssetID:Option, + pub ValidatedAssetVersion:Option, pub Completed:bool, pub TargetAssetID:u64, pub StatusID:MapfixStatus, From 231c11632bd7979dbf397c825f5fcffdf07582e8 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 19:00:57 -0700 Subject: [PATCH 23/27] openapi: add missing fields --- openapi.yaml | 10 ++++++ pkg/api/oas_json_gen.go | 54 +++++++++++++++++++++++++++------ pkg/api/oas_schemas_gen.go | 48 +++++++++++++++++++++-------- pkg/api/oas_validators_gen.go | 54 +++++++++++++++++++++++++++++++++ submissions-api-rs/src/types.rs | 2 ++ 5 files changed, 145 insertions(+), 23 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 0c8faf5..29b316f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1685,6 +1685,8 @@ components: - Submitter - AssetID - AssetVersion +# - ValidatedAssetID +# - ValidatedAssetVersion - Completed - TargetAssetID - StatusID @@ -1725,6 +1727,14 @@ components: type: integer format: int64 minimum: 0 + ValidatedAssetID: + type: integer + format: int64 + minimum: 0 + ValidatedAssetVersion: + type: integer + format: int64 + minimum: 0 Completed: type: boolean TargetAssetID: diff --git a/pkg/api/oas_json_gen.go b/pkg/api/oas_json_gen.go index 11a8678..7ee6f05 100644 --- a/pkg/api/oas_json_gen.go +++ b/pkg/api/oas_json_gen.go @@ -726,6 +726,18 @@ func (s *Mapfix) encodeFields(e *jx.Encoder) { e.FieldStart("AssetVersion") e.Int64(s.AssetVersion) } + { + if s.ValidatedAssetID.Set { + e.FieldStart("ValidatedAssetID") + s.ValidatedAssetID.Encode(e) + } + } + { + if s.ValidatedAssetVersion.Set { + e.FieldStart("ValidatedAssetVersion") + s.ValidatedAssetVersion.Encode(e) + } + } { e.FieldStart("Completed") e.Bool(s.Completed) @@ -744,7 +756,7 @@ func (s *Mapfix) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfMapfix = [13]string{ +var jsonFieldsNameOfMapfix = [15]string{ 0: "ID", 1: "DisplayName", 2: "Creator", @@ -754,10 +766,12 @@ var jsonFieldsNameOfMapfix = [13]string{ 6: "Submitter", 7: "AssetID", 8: "AssetVersion", - 9: "Completed", - 10: "TargetAssetID", - 11: "StatusID", - 12: "Description", + 9: "ValidatedAssetID", + 10: "ValidatedAssetVersion", + 11: "Completed", + 12: "TargetAssetID", + 13: "StatusID", + 14: "Description", } // Decode decodes Mapfix from json. @@ -877,8 +891,28 @@ func (s *Mapfix) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"AssetVersion\"") } + case "ValidatedAssetID": + if err := func() error { + s.ValidatedAssetID.Reset() + if err := s.ValidatedAssetID.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"ValidatedAssetID\"") + } + case "ValidatedAssetVersion": + if err := func() error { + s.ValidatedAssetVersion.Reset() + if err := s.ValidatedAssetVersion.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"ValidatedAssetVersion\"") + } case "Completed": - requiredBitSet[1] |= 1 << 1 + requiredBitSet[1] |= 1 << 3 if err := func() error { v, err := d.Bool() s.Completed = bool(v) @@ -890,7 +924,7 @@ func (s *Mapfix) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"Completed\"") } case "TargetAssetID": - requiredBitSet[1] |= 1 << 2 + requiredBitSet[1] |= 1 << 4 if err := func() error { v, err := d.Int64() s.TargetAssetID = int64(v) @@ -902,7 +936,7 @@ func (s *Mapfix) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"TargetAssetID\"") } case "StatusID": - requiredBitSet[1] |= 1 << 3 + requiredBitSet[1] |= 1 << 5 if err := func() error { v, err := d.Int32() s.StatusID = int32(v) @@ -914,7 +948,7 @@ func (s *Mapfix) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"StatusID\"") } case "Description": - requiredBitSet[1] |= 1 << 4 + requiredBitSet[1] |= 1 << 6 if err := func() error { v, err := d.Str() s.Description = string(v) @@ -936,7 +970,7 @@ func (s *Mapfix) Decode(d *jx.Decoder) error { var failures []validate.FieldError for i, mask := range [2]uint8{ 0b11111111, - 0b00011111, + 0b01111001, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. diff --git a/pkg/api/oas_schemas_gen.go b/pkg/api/oas_schemas_gen.go index 13fe069..0713b61 100644 --- a/pkg/api/oas_schemas_gen.go +++ b/pkg/api/oas_schemas_gen.go @@ -462,19 +462,21 @@ func (s *Map) SetModes(val uint32) { // Ref: #/components/schemas/Mapfix type Mapfix struct { - ID int64 `json:"ID"` - DisplayName string `json:"DisplayName"` - Creator string `json:"Creator"` - GameID int32 `json:"GameID"` - CreatedAt int64 `json:"CreatedAt"` - UpdatedAt int64 `json:"UpdatedAt"` - Submitter int64 `json:"Submitter"` - AssetID int64 `json:"AssetID"` - AssetVersion int64 `json:"AssetVersion"` - Completed bool `json:"Completed"` - TargetAssetID int64 `json:"TargetAssetID"` - StatusID int32 `json:"StatusID"` - Description string `json:"Description"` + ID int64 `json:"ID"` + DisplayName string `json:"DisplayName"` + Creator string `json:"Creator"` + GameID int32 `json:"GameID"` + CreatedAt int64 `json:"CreatedAt"` + UpdatedAt int64 `json:"UpdatedAt"` + Submitter int64 `json:"Submitter"` + AssetID int64 `json:"AssetID"` + AssetVersion int64 `json:"AssetVersion"` + ValidatedAssetID OptInt64 `json:"ValidatedAssetID"` + ValidatedAssetVersion OptInt64 `json:"ValidatedAssetVersion"` + Completed bool `json:"Completed"` + TargetAssetID int64 `json:"TargetAssetID"` + StatusID int32 `json:"StatusID"` + Description string `json:"Description"` } // GetID returns the value of ID. @@ -522,6 +524,16 @@ func (s *Mapfix) GetAssetVersion() int64 { return s.AssetVersion } +// GetValidatedAssetID returns the value of ValidatedAssetID. +func (s *Mapfix) GetValidatedAssetID() OptInt64 { + return s.ValidatedAssetID +} + +// GetValidatedAssetVersion returns the value of ValidatedAssetVersion. +func (s *Mapfix) GetValidatedAssetVersion() OptInt64 { + return s.ValidatedAssetVersion +} + // GetCompleted returns the value of Completed. func (s *Mapfix) GetCompleted() bool { return s.Completed @@ -587,6 +599,16 @@ func (s *Mapfix) SetAssetVersion(val int64) { s.AssetVersion = val } +// SetValidatedAssetID sets the value of ValidatedAssetID. +func (s *Mapfix) SetValidatedAssetID(val OptInt64) { + s.ValidatedAssetID = val +} + +// SetValidatedAssetVersion sets the value of ValidatedAssetVersion. +func (s *Mapfix) SetValidatedAssetVersion(val OptInt64) { + s.ValidatedAssetVersion = val +} + // SetCompleted sets the value of Completed. func (s *Mapfix) SetCompleted(val bool) { s.Completed = val diff --git a/pkg/api/oas_validators_gen.go b/pkg/api/oas_validators_gen.go index b636337..c2a04eb 100644 --- a/pkg/api/oas_validators_gen.go +++ b/pkg/api/oas_validators_gen.go @@ -390,6 +390,60 @@ func (s *Mapfix) Validate() error { Error: err, }) } + if err := func() error { + if value, ok := s.ValidatedAssetID.Get(); ok { + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(value)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "ValidatedAssetID", + Error: err, + }) + } + if err := func() error { + if value, ok := s.ValidatedAssetVersion.Get(); ok { + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(value)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "ValidatedAssetVersion", + Error: err, + }) + } if err := func() error { if err := (validate.Int{ MinSet: true, diff --git a/submissions-api-rs/src/types.rs b/submissions-api-rs/src/types.rs index ff3bf67..a65f71d 100644 --- a/submissions-api-rs/src/types.rs +++ b/submissions-api-rs/src/types.rs @@ -370,6 +370,8 @@ pub struct SubmissionResponse{ pub Submitter:u64, pub AssetID:u64, pub AssetVersion:u64, + pub ValidatedAssetID:Option, + pub ValidatedAssetVersion:Option, pub UploadedAssetID:u64, pub StatusID:SubmissionStatus, } From 2639abc7c8c40a65c48983f22633709ff065b987 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 19:02:00 -0700 Subject: [PATCH 24/27] submissions-api-rs: v0.9.1 fixes --- Cargo.lock | 2 +- submissions-api-rs/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b3d680..d7a9788 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2057,7 +2057,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "submissions-api" -version = "0.9.0" +version = "0.9.1" dependencies = [ "chrono", "reqwest", diff --git a/submissions-api-rs/Cargo.toml b/submissions-api-rs/Cargo.toml index 6252bf3..1d38d16 100644 --- a/submissions-api-rs/Cargo.toml +++ b/submissions-api-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "submissions-api" -version = "0.9.0" +version = "0.9.1" edition = "2024" publish = ["strafesnet"] repository = "https://git.itzana.me/StrafesNET/maps-service" From 2878467cbff525c9305d40defd161e2bf391f045 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 19:35:28 -0700 Subject: [PATCH 25/27] backend: add forgotten permission --- pkg/web_api/mapfixes.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/web_api/mapfixes.go b/pkg/web_api/mapfixes.go index 245d257..7cb77be 100644 --- a/pkg/web_api/mapfixes.go +++ b/pkg/web_api/mapfixes.go @@ -1199,5 +1199,19 @@ func (svc *Service) ListMapfixAuditEvents(ctx context.Context, params api.ListMa // // GET /mapfixes/{MapfixID}/audit-events func (svc *Service) MigrateMapfixes(ctx context.Context) error { + userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) + if !ok { + return ErrUserInfo + } + + has_role, err := userInfo.HasRoleSubmissionRelease() + if err != nil { + return err + } + // check if caller has required role + if !has_role { + return ErrPermissionDeniedNeedRoleSubmissionRelease + } + return svc.inner.MigrateMapfixes(ctx) } From 1ce09e3f9b319ec55deafce2179174654930f152 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 19:35:39 -0700 Subject: [PATCH 26/27] docker: add env vars to compose.yml --- compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose.yaml b/compose.yaml index 3f149e2..b04a6ea 100644 --- a/compose.yaml +++ b/compose.yaml @@ -64,6 +64,8 @@ services: - ROBLOX_GROUP_ID=17032139 # "None" is special case string value - API_HOST_INTERNAL=http://submissions:8083/v1 - NATS_HOST=nats:4222 + - LOAD_ASSET_VERSION_PLACE_ID=14001440964 + - LOAD_ASSET_VERSION_UNIVERSE_ID=4850603885 depends_on: - nats # note: this races the submissions which creates a nats stream From 55b79b8f9bbaa7de7515f14d6e61b5367a3cbb8e Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Fri, 15 Aug 2025 19:40:24 -0700 Subject: [PATCH 27/27] backend: typo --- pkg/datastore/gormstore/mapfixes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/datastore/gormstore/mapfixes.go b/pkg/datastore/gormstore/mapfixes.go index af59fa4..5845645 100644 --- a/pkg/datastore/gormstore/mapfixes.go +++ b/pkg/datastore/gormstore/mapfixes.go @@ -165,6 +165,6 @@ func (env *Mapfixes) Migrate(ctx context.Context) error { Count(&count). Error - log.Println("Mafixes Migration rows affected:",count) + log.Println("Mapfixes Migration rows affected:",count) return err }