diff --git a/Cargo.lock b/Cargo.lock index b728c94..d7a9788 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" @@ -784,7 +784,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -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", ] @@ -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", @@ -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", @@ -1441,7 +1441,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1520,9 +1520,9 @@ dependencies = [ [[package]] name = "rbx_asset" -version = "0.4.9" +version = "0.4.10" source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" -checksum = "32c7c8efca16fc09ac623ea41cde2bf48e2da761ac8edd575b0b7022f5dc5bd5" +checksum = "a711a8c43b4bbcd3c72832e51a680e407b3a062e1ddc66cb90e57b86c0e65f80" dependencies = [ "bytes", "chrono", @@ -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", @@ -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]] @@ -1834,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" @@ -2008,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" @@ -2056,7 +2057,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "submissions-api" -version = "0.8.2" +version = "0.9.1" dependencies = [ "chrono", "reqwest", @@ -2074,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", @@ -2135,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]] @@ -2155,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", @@ -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/Makefile b/Makefile index 8ca1e28..512d217 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,6 @@ docker-validator: make build-validator make image-validator docker-frontend: - make build-frontend make image-frontend docker: docker-backend docker-validator docker-frontend diff --git a/README.md b/README.md index 7930a81..0f71937 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,11 @@ Prerequisite: rust installed Environment Variables: - ROBLOX_GROUP_ID - RBXCOOKIE +- RBX_API_KEY - API_HOST_INTERNAL - NATS_HOST +- LOAD_ASSET_VERSION_PLACE_ID +- LOAD_ASSET_VERSION_UNIVERSE_ID #### License diff --git a/compose.yaml b/compose.yaml index 3f149e2..50ed183 100644 --- a/compose.yaml +++ b/compose.yaml @@ -34,7 +34,7 @@ services: "--data-rpc-host","dataservice:9000", ] env_file: - - ../auth-compose/strafesnet_staging.env + - ~/auth-compose/strafesnet_staging.env depends_on: - authrpc - nats @@ -59,11 +59,13 @@ services: maptest-validator container_name: validation env_file: - - ../auth-compose/strafesnet_staging.env + - ~/auth-compose/strafesnet_staging.env environment: - 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 @@ -103,7 +105,7 @@ services: - REDIS_ADDR=authredis:6379 - RBX_GROUP_ID=17032139 env_file: - - ../auth-compose/auth-service.env + - ~/auth-compose/auth-service.env depends_on: - authredis networks: @@ -117,7 +119,7 @@ services: environment: - REDIS_ADDR=authredis:6379 env_file: - - ../auth-compose/auth-service.env + - ~/auth-compose/auth-service.env depends_on: - authredis networks: diff --git a/go.mod b/go.mod index 4e1235c..876de48 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-20250815013325-1c84f73bdcb1 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..344bdc5 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-20250815013325-1c84f73bdcb1 h1:imXibfeYcae6og0TTDUFRQ3CQtstGjIoLbCn+pezD2o= +git.itzana.me/strafesnet/go-grpc v0.0.0-20250815013325-1c84f73bdcb1/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/openapi.yaml b/openapi.yaml index d233778..29b316f 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: @@ -312,6 +318,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 @@ -587,7 +608,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 +625,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 +640,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 @@ -698,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: @@ -1067,7 +1128,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 +1145,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 @@ -1101,7 +1162,7 @@ paths: $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 @@ -1624,6 +1685,8 @@ components: - Submitter - AssetID - AssetVersion +# - ValidatedAssetID +# - ValidatedAssetVersion - Completed - TargetAssetID - StatusID @@ -1664,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_client_gen.go b/pkg/api/oas_client_gen.go index bc9a36b..361a9c1 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 @@ -159,7 +171,8 @@ type Invoker interface { ActionSubmissionTriggerValidate(ctx context.Context, params ActionSubmissionTriggerValidateParams) 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 @@ -301,9 +314,15 @@ 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. + // Release a set of uploaded maps. Role SubmissionRelease. // // POST /release-submissions ReleaseSubmissions(ctx context.Context, request []ReleaseInfo) error @@ -1157,6 +1176,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 +1550,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 +1796,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 +3039,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 { @@ -3020,7 +3287,8 @@ func (c *Client) sendActionSubmissionTriggerValidate(ctx context.Context, params // 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 { @@ -5184,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{ @@ -6063,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{ @@ -6121,9 +6423,114 @@ 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. +// 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..837522f 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{ @@ -4129,7 +4519,8 @@ func (s *Server) handleActionSubmissionTriggerValidateRequest(args [1]string, ar // 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 +4654,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{ @@ -7525,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", @@ -8374,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", @@ -8433,9 +8832,189 @@ 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. +// 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 +9153,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_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_operations_gen.go b/pkg/api/oas_operations_gen.go index b5a33a8..dcd30ac 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" @@ -51,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_parameters_gen.go b/pkg/api/oas_parameters_gen.go index ac8c0f6..5107683 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. @@ -2972,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 @@ -3051,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", @@ -3568,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{ @@ -5221,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 @@ -5300,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", @@ -5817,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/api/oas_response_decoders_gen.go b/pkg/api/oas_response_decoders_gen.go index 958d0d9..c4e0e14 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: @@ -3595,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 fb923c6..85976a5 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)) @@ -484,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 1dc226d..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, '/') @@ -318,6 +344,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 +492,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" { @@ -1532,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, '/') @@ -1762,6 +1862,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 +1923,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 +2022,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 +2108,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 +2284,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 @@ -2753,7 +2901,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 +3062,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..0713b61 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{} @@ -456,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. @@ -516,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 @@ -581,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 @@ -664,6 +692,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 100c173..ad72e8e 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{}, @@ -67,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 bc8d72e..53b80df 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 @@ -138,7 +150,8 @@ type Handler interface { ActionSubmissionTriggerValidate(ctx context.Context, params ActionSubmissionTriggerValidateParams) 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 @@ -280,9 +293,15 @@ 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. + // 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..63f35e7 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 { @@ -206,7 +224,8 @@ func (UnimplementedHandler) ActionSubmissionTriggerValidate(ctx context.Context, // 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 { @@ -420,9 +439,18 @@ 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. +// Release a set of uploaded maps. Role SubmissionRelease. // // POST /release-submissions func (UnimplementedHandler) ReleaseSubmissions(ctx context.Context, req []ReleaseInfo) error { 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/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..0d1fc27 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). + Update("status_id", migrate_to). + Count(&count). + Error + + log.Println("Mapfixes Migration rows affected:",count) + return err +} 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/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/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/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 bf33fcc..eb5d8ea 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, @@ -184,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 @@ -253,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/operations.go b/pkg/validator_controller/operations.go index e95f10b..993d458 100644 --- a/pkg/validator_controller/operations.go +++ b/pkg/validator_controller/operations.go @@ -19,6 +19,18 @@ func NewOperationsController( } } +func (svc *Operations) Success(ctx context.Context, params *validator.OperationSuccessRequest) (*validator.NullResponse, error) { + success_params := service.NewOperationCompleteParams( + params.Path, + ) + err := svc.inner.CompleteOperation(ctx, int32(params.OperationID), success_params) + if err != nil { + return nil, err + } + + return &validator.NullResponse{}, nil +} + // ActionOperationFailed implements actionOperationFailed operation. // // Fail the specified OperationID with a StatusMessage. diff --git a/pkg/validator_controller/submissions.go b/pkg/validator_controller/submissions.go index 1a373b3..999fa02 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" @@ -24,7 +25,7 @@ func NewSubmissionsController( } var( - // prevent two mapfixes with same asset id + // prevent two submissions with same asset id ActiveSubmissionStatuses = []model.SubmissionStatus{ model.SubmissionStatusUploading, model.SubmissionStatusValidated, @@ -202,7 +203,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 @@ -273,6 +274,68 @@ 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.SubmissionStatusUploaded}, update) + 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 048325c..7cb77be 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, @@ -193,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)) } @@ -786,6 +791,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. @@ -1066,3 +1192,26 @@ 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 { + 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) +} diff --git a/pkg/web_api/submissions.go b/pkg/web_api/submissions.go index a167454..ef8bdd7 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 @@ -236,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)) } @@ -1053,6 +1049,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 @@ -1074,32 +1075,39 @@ 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 - } + // 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 + } - // 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 - } + // 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 diff --git a/submissions-api-rs/Cargo.toml b/submissions-api-rs/Cargo.toml index 9982ecf..1d38d16 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.1" edition = "2024" publish = ["strafesnet"] repository = "https://git.itzana.me/StrafesNET/maps-service" diff --git a/submissions-api-rs/src/external.rs b/submissions-api-rs/src/external.rs index e32655c..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)?; @@ -178,6 +220,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..a65f71d 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:Option, + pub ValidatedAssetVersion:Option, + 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{ @@ -286,6 +353,7 @@ pub struct GetSubmissionsRequest<'a>{ pub GameID:Option, pub Submitter:Option, pub AssetID:Option, + pub AssetVersion:Option, pub UploadedAssetID:Option, pub StatusID:Option, } @@ -302,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, } diff --git a/validation/Cargo.toml b/validation/Cargo.toml index 1421471..df64ac1 100644 --- a/validation/Cargo.toml +++ b/validation/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] async-nats = "0.42.0" futures = "0.3.31" -rbx_asset = { version = "0.4.9", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" } +rbx_asset = { version = "0.4.10", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" } rbx_binary = "1.0.0" rbx_dom_weak = "3.0.0" rbx_reflection_database = "1.0.3" @@ -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/check.rs b/validation/src/check.rs index 24db010..0d2939f 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -214,6 +214,15 @@ impl std::fmt::Display for WormholeElement{ } } +fn count_sequential(modes:&HashMap>)->usize{ + for mode_id in 0..modes.len(){ + if !modes.contains_key(&ModeID(mode_id as u64)){ + return mode_id; + } + } + return modes.len(); +} + /// Count various map elements #[derive(Default)] struct Counts<'a>{ @@ -233,6 +242,24 @@ pub struct ModelInfo<'a>{ counts:Counts<'a>, unanchored_parts:Vec<&'a Instance>, } +impl ModelInfo<'_>{ + pub fn count_modes(&self)->Option{ + let start_zones_count=self.counts.mode_start_counts.len(); + let finish_zones_count=self.counts.mode_finish_counts.len(); + let sequential_start_zones=count_sequential(&self.counts.mode_start_counts); + let sequential_finish_zones=count_sequential(&self.counts.mode_finish_counts); + // all counts must match + if start_zones_count==finish_zones_count + && sequential_start_zones==sequential_finish_zones + && start_zones_count==sequential_start_zones + && finish_zones_count==sequential_finish_zones + { + Some(start_zones_count) + }else{ + None + } + } +} pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_dom_weak::Instance)->ModelInfo<'a>{ // extract model info @@ -525,14 +552,6 @@ impl<'a> ModelInfo<'a>{ .check(&self.counts.mode_start_counts); // There must not be non-sequential modes. If Bonus100 exists, Bonuses 1-99 had better also exist. - fn count_sequential(modes:&HashMap>)->usize{ - for mode_id in 0..modes.len(){ - if !modes.contains_key(&ModeID(mode_id as u64)){ - return mode_id; - } - } - return modes.len(); - } let modes_sequential={ let sequential=count_sequential(&self.counts.mode_start_counts); if sequential==self.counts.mode_start_counts.len(){ 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..e032831 100644 --- a/validation/src/grpc/submissions.rs +++ b/validation/src/grpc/submissions.rs @@ -18,6 +18,8 @@ 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); } diff --git a/validation/src/main.rs b/validation/src/main.rs index 980e2f5..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; @@ -47,24 +50,44 @@ async fn main()->Result<(),StartupError>{ }, Err(e)=>panic!("{e}: ROBLOX_GROUP_ID env required"), }; + let load_asset_version_place_id=std::env::var("LOAD_ASSET_VERSION_PLACE_ID").expect("LOAD_ASSET_VERSION_PLACE_ID env required").parse().expect("LOAD_ASSET_VERSION_PLACE_ID int parse failed"); + let load_asset_version_universe_id=std::env::var("LOAD_ASSET_VERSION_UNIVERSE_ID").expect("LOAD_ASSET_VERSION_UNIVERSE_ID env required").parse().expect("LOAD_ASSET_VERSION_UNIVERSE_ID int parse failed"); // 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"); let endpoint=tonic::transport::Endpoint::new(api_host_internal).map_err(StartupError::API)?; let channel=endpoint.connect_lazy(); - let mapfixes=crate::grpc::mapfixes::ValidatorMapfixesServiceClient::new(channel.clone()); - let operations=crate::grpc::operations::ValidatorOperationsServiceClient::new(channel.clone()); - let scripts=crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone()); - let script_policy=crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone()); - let submissions=crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel); - let message_handler=message_handler::MessageHandler::new(cloud_context,cookie_context,group_id,mapfixes,operations,scripts,script_policy,submissions); + let mapfixes=crate::grpc::mapfixes::Service::new(crate::grpc::mapfixes::ValidatorMapfixesServiceClient::new(channel.clone())); + let operations=crate::grpc::operations::Service::new(crate::grpc::operations::ValidatorOperationsServiceClient::new(channel.clone())); + 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_runtime, + mapfixes, + operations, + scripts, + script_policy, + submissions, + }; // nats let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required"); diff --git a/validation/src/message_handler.rs b/validation/src/message_handler.rs index 4508369..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), @@ -30,7 +32,9 @@ fn from_slice<'a,T:serde::de::Deserialize<'a>>(slice:&'a [u8])->Result, + 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, @@ -39,27 +43,6 @@ pub struct MessageHandler{ } impl MessageHandler{ - pub fn new( - cloud_context:rbx_asset::cloud::Context, - cookie_context:rbx_asset::cookie::Context, - group_id:Option, - mapfixes:crate::grpc::mapfixes::ValidatorMapfixesServiceClient, - operations:crate::grpc::operations::ValidatorOperationsServiceClient, - scripts:crate::grpc::scripts::ValidatorScriptsServiceClient, - script_policy:crate::grpc::script_policy::ValidatorScriptPolicyServiceClient, - submissions:crate::grpc::submissions::ValidatorSubmissionsServiceClient, - )->Self{ - Self{ - cloud_context, - cookie_context, - group_id, - mapfixes:crate::grpc::mapfixes::Service::new(mapfixes), - operations:crate::grpc::operations::Service::new(operations), - scripts:crate::grpc::scripts::Service::new(scripts), - script_policy:crate::grpc::script_policy::Service::new(script_policy), - submissions:crate::grpc::submissions::Service::new(submissions), - } - } pub async fn handle_message_result(&self,message_result:MessageResult)->Result<(),HandleMessageError>{ let message=message_result.map_err(HandleMessageError::Messages)?; message.double_ack().await.map_err(HandleMessageError::DoubleAck)?; @@ -68,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/rbx_util.rs b/validation/src/rbx_util.rs index 14297ed..a8185f4 100644 --- a/validation/src/rbx_util.rs +++ b/validation/src/rbx_util.rs @@ -1,4 +1,3 @@ - #[allow(dead_code)] #[derive(Debug)] pub enum ReadDomError{ @@ -112,3 +111,21 @@ pub fn get_mapinfo<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&rbx_dom_wea game_id:model_instance.name.parse(), } } + +pub async fn get_luau_result_exp_backoff( + context:&rbx_asset::cloud::Context, + luau_session:&rbx_asset::cloud::LuauSessionResponse +)->Result,rbx_asset::cloud::LuauSessionError>{ + const BACKOFF_MUL:f32=1.395_612_5;//exp(1/3) + let mut backoff=1000f32; + loop{ + match luau_session.try_get_result(context).await{ + //try again when the operation is not done + Err(rbx_asset::cloud::LuauSessionError::NotDone)=>(), + //return all other results + other_result=>return other_result, + } + tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await; + backoff*=BACKOFF_MUL; + } +} diff --git a/validation/src/release.rs b/validation/src/release.rs new file mode 100644 index 0000000..df60c3a --- /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\")\nreturn\n".to_string(); + for asset in assets{ + use std::fmt::Write; + write!(script,"InsertService:GetLatestAssetVersionAsync({asset}),\n").unwrap(); + } + + let session=rbx_asset::cloud::LuauSessionCreate{ + script:&script[..script.len()-2], + 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| + match load_asset_version.as_u64(){ + Some(version)=>Ok(version), + None=>Err(LoadAssetVersionsError::NonPositiveNumber(load_asset_version)) + } + ).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 new file mode 100644 index 0000000..0940a49 --- /dev/null +++ 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..8d77eef --- /dev/null +++ b/validation/src/release_submissions_batch.rs @@ -0,0 +1,227 @@ +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 + + // 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(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::)>>(); + + // 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(()) + } +} diff --git a/validation/src/upload_mapfix.rs b/validation/src/upload_mapfix.rs index 8e567cd..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,31 +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)?; - // that's it, the database entry does not need to be changed. - - // 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(()) } 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)?; }, 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/_components/statusChip.tsx b/web/src/app/_components/statusChip.tsx index 05e36ff..f4b18a2 100644 --- a/web/src/app/_components/statusChip.tsx +++ b/web/src/app/_components/statusChip.tsx @@ -53,6 +53,11 @@ export const StatusChip = ({status}: { status: number }) => { icon = ; label = 'Uploaded'; break; + case 11: + color = 'info'; + icon = ; + label = 'Releasing'; + break; case 9: color = 'error'; icon = ; @@ -84,4 +89,4 @@ export const StatusChip = ({status}: { status: number }) => { }} /> ); -}; \ No newline at end of file +}; diff --git a/web/src/app/ts/AuditEvent.ts b/web/src/app/ts/AuditEvent.ts index cb99149..bc8067e 100644 --- a/web/src/app/ts/AuditEvent.ts +++ b/web/src/app/ts/AuditEvent.ts @@ -1,4 +1,4 @@ -import { SubmissionStatusToString } from "./Submission"; +import { MapfixStatusToString } from "./Mapfix"; // Shared audit event types export const enum AuditEventType { @@ -79,7 +79,7 @@ export function decodeAuditEvent(event: AuditEvent): string { switch (event.EventType) { case AuditEventType.Action:{ const data = event.EventData as AuditEventDataAction; - return `Changed status to ${SubmissionStatusToString(data.target_status)}`; + return `Changed status to ${MapfixStatusToString(data.target_status)}`; }case AuditEventType.Comment:{ const data = event.EventData as AuditEventDataComment; return data.comment; 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..2700ff0 100644 --- a/web/src/app/ts/Status.ts +++ b/web/src/app/ts/Status.ts @@ -1,19 +1,20 @@ -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 + 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[]) => { return statusValues.includes(status); -}; \ No newline at end of file +};