Compare commits
75 Commits
rm-alloc
...
feature/ao
| Author | SHA1 | Date | |
|---|---|---|---|
| b31ba5d278 | |||
| 29cbab545f | |||
| 60ef6a5df6 | |||
| 1688178cd3 | |||
| ab5f1289c4 | |||
| 2c4627d467 | |||
| 34017ee771 | |||
| e41d34dd3d | |||
| f49e27e230 | |||
| d500462fc7 | |||
| ee2bc94312 | |||
| 84edc71574 | |||
| 7c5d8a2163 | |||
| 7eaa84a0ed | |||
| cf0cf9da7a | |||
| 74565e567a | |||
| ea65794255 | |||
| 58706a5687 | |||
| efeb525e19 | |||
|
5a1fe60a7b
|
|||
| 01cfe67848 | |||
| a19bc4d380 | |||
| ae006565d6 | |||
| 57bca99109 | |||
| cd09c9b18e | |||
| e48cbaff72 | |||
| 140d58b808 | |||
| ba761549b8 | |||
| 86643fef8d | |||
| 96af864c5e | |||
| 7db89fd99b | |||
| f2bb1b078d | |||
| 66878fba4e | |||
| bda99550be | |||
| 8a216c7e82 | |||
| e5277c05a1 | |||
| e4af76cfd4 | |||
| 30db1cc375 | |||
| b50c84f8cf | |||
| 7589ef7df6 | |||
| 8ab8c441b0 | |||
| a26b228ebe | |||
| 3654755540 | |||
| c2b50ffab2 | |||
| 75756917b1 | |||
| 8989c08857 | |||
| b2232f4177 | |||
| 7d1c4d2b6c | |||
| ca401d4b96 | |||
|
9ab80931bf
|
|||
|
09022e7292
|
|||
| 47c0fff0ec | |||
| b7c28616ad | |||
| 89ab25dfb9 | |||
| b0b5ff0725 | |||
| 0532965d37 | |||
| f59979987f | |||
| a232269d54 | |||
| a7c4ca4b49 | |||
| ca9f82a5aa | |||
| e1a2f6f075 | |||
| dad904cd86 | |||
| ad7117a69c | |||
| d566591ea6 | |||
| 424ef6238b | |||
| 0f0ab4d3e0 | |||
| 3e2d782289 | |||
| dc446c545f | |||
|
e234a87d05
|
|||
| 8ab772ea81 | |||
| 9b58b1d26a | |||
| 7689001e74 | |||
| e89abed3d5 | |||
| b792d33164 | |||
| 929b5949f0 |
@@ -26,6 +26,7 @@ func main() {
|
||||
app.Commands = []*cli.Command{
|
||||
cmds.NewServeCommand(),
|
||||
cmds.NewApiCommand(),
|
||||
cmds.NewAORCommand(),
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
"--data-rpc-host","dataservice:9000",
|
||||
]
|
||||
env_file:
|
||||
- ~/auth-compose/strafesnet_staging.env
|
||||
- /home/quat/auth-compose/strafesnet_staging.env
|
||||
depends_on:
|
||||
- authrpc
|
||||
- nats
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
maptest-validator
|
||||
container_name: validation
|
||||
env_file:
|
||||
- ~/auth-compose/strafesnet_staging.env
|
||||
- /home/quat/auth-compose/strafesnet_staging.env
|
||||
environment:
|
||||
- ROBLOX_GROUP_ID=17032139 # "None" is special case string value
|
||||
- API_HOST_INTERNAL=http://submissions:8083/v1
|
||||
@@ -105,7 +105,7 @@ services:
|
||||
- REDIS_ADDR=authredis:6379
|
||||
- RBX_GROUP_ID=17032139
|
||||
env_file:
|
||||
- ~/auth-compose/auth-service.env
|
||||
- /home/quat/auth-compose/auth-service.env
|
||||
depends_on:
|
||||
- authredis
|
||||
networks:
|
||||
@@ -119,7 +119,7 @@ services:
|
||||
environment:
|
||||
- REDIS_ADDR=authredis:6379
|
||||
env_file:
|
||||
- ~/auth-compose/auth-service.env
|
||||
- /home/quat/auth-compose/auth-service.env
|
||||
depends_on:
|
||||
- authredis
|
||||
networks:
|
||||
|
||||
45
go.mod
45
go.mod
@@ -11,17 +11,18 @@ require (
|
||||
github.com/dchest/siphash v1.2.3
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-faster/errors v0.7.1
|
||||
github.com/go-faster/jx v1.1.0
|
||||
github.com/go-faster/jx v1.2.0
|
||||
github.com/nats-io/nats.go v1.37.0
|
||||
github.com/ogen-go/ogen v1.2.1
|
||||
github.com/ogen-go/ogen v1.18.0
|
||||
github.com/redis/go-redis/v9 v9.10.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/urfave/cli/v2 v2.27.6
|
||||
go.opentelemetry.io/otel v1.32.0
|
||||
go.opentelemetry.io/otel/metric v1.32.0
|
||||
go.opentelemetry.io/otel/trace v1.32.0
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
go.opentelemetry.io/otel/metric v1.39.0
|
||||
go.opentelemetry.io/otel/trace v1.39.0
|
||||
google.golang.org/grpc v1.48.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.25.12
|
||||
@@ -33,9 +34,11 @@ require (
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
@@ -55,7 +58,7 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.6 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
@@ -65,36 +68,38 @@ require (
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-faster/yaml v0.4.6 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
// github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
69
go.sum
69
go.sum
@@ -14,12 +14,18 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
@@ -39,8 +45,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -49,6 +59,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
@@ -63,11 +75,13 @@ github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AY
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
|
||||
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
||||
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
|
||||
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
|
||||
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
||||
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
@@ -113,8 +127,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -140,6 +154,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
@@ -159,6 +175,8 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
@@ -176,11 +194,15 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/ogen-go/ogen v1.2.1 h1:C5A0lvUMu2wl+eWIxnpXMWnuOJ26a2FyzR1CIC2qG0M=
|
||||
github.com/ogen-go/ogen v1.2.1/go.mod h1:P2zQdEu8UqaVRfD5GEFvl+9q63VjMLvDquq1wVbyInM=
|
||||
github.com/ogen-go/ogen v1.18.0 h1:6RQ7lFBjOeNaUWu4getfqIh4GJbEY4hqKuzDtec/g60=
|
||||
github.com/ogen-go/ogen v1.18.0/go.mod h1:dHFr2Wf6cA7tSxMI+zPC21UR5hAlDw8ZYUkK3PziURY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
||||
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
@@ -188,6 +210,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
@@ -204,8 +230,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
@@ -221,12 +248,14 @@ github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
@@ -234,6 +263,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
@@ -242,15 +273,21 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -266,6 +303,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -275,6 +314,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -293,6 +334,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -303,6 +346,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -312,6 +357,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
493
kind-setup.sh
Executable file
493
kind-setup.sh
Executable file
@@ -0,0 +1,493 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
CLUSTER_NAME="${KIND_CLUSTER_NAME:-maps-service-local}"
|
||||
INFRA_PATH="${INFRA_PATH:-$HOME/Documents/Projects/infra}"
|
||||
NAMESPACE="${NAMESPACE:-default}"
|
||||
REGISTRY_NAME="kind-registry"
|
||||
REGISTRY_PORT="5001"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
log_info "Checking dependencies..."
|
||||
local deps=("kind" "kubectl" "docker")
|
||||
for dep in "${deps[@]}"; do
|
||||
if ! command -v "$dep" &> /dev/null; then
|
||||
log_error "$dep is not installed. Please install it first."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
log_info "All dependencies are installed"
|
||||
}
|
||||
|
||||
# Create local container registry
|
||||
create_registry() {
|
||||
if [ "$(docker ps -q -f name=${REGISTRY_NAME})" ]; then
|
||||
log_info "Registry ${REGISTRY_NAME} already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(docker ps -aq -f name=${REGISTRY_NAME})" ]; then
|
||||
log_info "Starting existing registry ${REGISTRY_NAME}"
|
||||
docker start ${REGISTRY_NAME}
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Creating local registry ${REGISTRY_NAME}..."
|
||||
docker run -d --restart=always -p "127.0.0.1:${REGISTRY_PORT}:5000" --name "${REGISTRY_NAME}" registry:2
|
||||
}
|
||||
|
||||
# Create KIND cluster with registry
|
||||
create_cluster() {
|
||||
if kind get clusters | grep -q "^${CLUSTER_NAME}$"; then
|
||||
log_warn "Cluster ${CLUSTER_NAME} already exists"
|
||||
read -p "Do you want to delete and recreate it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "Deleting existing cluster..."
|
||||
kind delete cluster --name "${CLUSTER_NAME}"
|
||||
else
|
||||
log_info "Using existing cluster"
|
||||
kubectl config use-context "kind-${CLUSTER_NAME}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Creating KIND cluster ${CLUSTER_NAME}..."
|
||||
cat <<EOF | kind create cluster --name "${CLUSTER_NAME}" --config=-
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
containerdConfigPatches:
|
||||
- |-
|
||||
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:${REGISTRY_PORT}"]
|
||||
endpoint = ["http://${REGISTRY_NAME}:5000"]
|
||||
nodes:
|
||||
- role: control-plane
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: InitConfiguration
|
||||
nodeRegistration:
|
||||
kubeletExtraArgs:
|
||||
node-labels: "ingress-ready=true"
|
||||
extraPortMappings:
|
||||
- containerPort: 80
|
||||
hostPort: 80
|
||||
protocol: TCP
|
||||
- containerPort: 443
|
||||
hostPort: 443
|
||||
protocol: TCP
|
||||
- containerPort: 8080
|
||||
hostPort: 8080
|
||||
protocol: TCP
|
||||
- containerPort: 3000
|
||||
hostPort: 3000
|
||||
protocol: TCP
|
||||
EOF
|
||||
|
||||
# Connect the registry to the cluster network
|
||||
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${REGISTRY_NAME}")" = 'null' ]; then
|
||||
log_info "Connecting registry to cluster network..."
|
||||
docker network connect "kind" "${REGISTRY_NAME}"
|
||||
fi
|
||||
|
||||
# Document the local registry
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: local-registry-hosting
|
||||
namespace: kube-public
|
||||
data:
|
||||
localRegistryHosting.v1: |
|
||||
host: "localhost:${REGISTRY_PORT}"
|
||||
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
|
||||
EOF
|
||||
|
||||
log_info "KIND cluster created successfully"
|
||||
}
|
||||
|
||||
# Build Docker images
|
||||
build_images() {
|
||||
log_info "Building Docker images..."
|
||||
|
||||
log_info "Building backend..."
|
||||
make build-backend
|
||||
docker build -t localhost:${REGISTRY_PORT}/maptest-api:local .
|
||||
docker push localhost:${REGISTRY_PORT}/maptest-api:local
|
||||
|
||||
log_info "Building validator..."
|
||||
make build-validator
|
||||
docker build -f validation/Containerfile -t localhost:${REGISTRY_PORT}/maptest-validator:local .
|
||||
docker push localhost:${REGISTRY_PORT}/maptest-validator:local
|
||||
|
||||
log_info "Building frontend..."
|
||||
docker build web -f web/Containerfile -t localhost:${REGISTRY_PORT}/maptest-frontend:local .
|
||||
docker push localhost:${REGISTRY_PORT}/maptest-frontend:local
|
||||
|
||||
log_info "All images built and pushed to local registry"
|
||||
}
|
||||
|
||||
# Create secrets
|
||||
create_secrets() {
|
||||
log_info "Creating Kubernetes secrets..."
|
||||
|
||||
# Create dummy secrets for local development
|
||||
kubectl create secret generic cockroach-qtdb \
|
||||
--from-literal=HOST=data-postgres \
|
||||
--from-literal=PORT=5432 \
|
||||
--from-literal=USER=postgres \
|
||||
--from-literal=PASS=localpassword \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
kubectl create secret generic maptest-cookie \
|
||||
--from-literal=api=dummy-api-key \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
kubectl create secret generic auth-service-secrets \
|
||||
--from-literal=DISCORD_CLIENT_ID=dummy \
|
||||
--from-literal=DISCORD_CLIENT_SECRET=dummy \
|
||||
--from-literal=RBX_API_KEY=dummy \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
log_info "Secrets created"
|
||||
}
|
||||
|
||||
# Deploy dependencies
|
||||
deploy_dependencies() {
|
||||
log_info "Deploying dependencies..."
|
||||
|
||||
# Deploy PostgreSQL (manual deployment)
|
||||
log_info "Deploying PostgreSQL..."
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: data-postgres
|
||||
spec:
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
selector:
|
||||
app: data-postgres
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: data-postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: data-postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: data-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:15
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: postgres
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: localpassword
|
||||
- name: POSTGRES_DB
|
||||
value: postgres
|
||||
volumeMounts:
|
||||
- name: postgres-storage
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- name: postgres-storage
|
||||
emptyDir: {}
|
||||
EOF
|
||||
|
||||
# Deploy Redis (using a simple deployment)
|
||||
log_info "Deploying Redis..."
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-master
|
||||
spec:
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: redis
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:latest
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
EOF
|
||||
|
||||
# Deploy NATS
|
||||
log_info "Deploying NATS..."
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nats
|
||||
spec:
|
||||
ports:
|
||||
- port: 4222
|
||||
targetPort: 4222
|
||||
selector:
|
||||
app: nats
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nats
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nats
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nats
|
||||
spec:
|
||||
containers:
|
||||
- name: nats
|
||||
image: nats:latest
|
||||
args: ["-js"]
|
||||
ports:
|
||||
- containerPort: 4222
|
||||
EOF
|
||||
|
||||
# Deploy Auth Service (if needed)
|
||||
if [ -d "${INFRA_PATH}/applications/auth-service/base" ]; then
|
||||
log_info "Deploying auth-service..."
|
||||
kubectl apply -k "${INFRA_PATH}/applications/auth-service/base" || log_warn "Auth service deployment failed, continuing..."
|
||||
fi
|
||||
|
||||
# Deploy Data Service (if needed)
|
||||
if [ -d "${INFRA_PATH}/applications/data-service/base" ]; then
|
||||
log_info "Deploying data-service..."
|
||||
kubectl apply -k "${INFRA_PATH}/applications/data-service/base" || log_warn "Data service deployment failed, continuing..."
|
||||
fi
|
||||
|
||||
log_info "Waiting for dependencies to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l app=data-postgres --timeout=120s || log_warn "PostgreSQL not ready yet"
|
||||
kubectl wait --for=condition=ready pod -l app=nats --timeout=60s || log_warn "NATS not ready yet"
|
||||
}
|
||||
|
||||
# Deploy maps-service
|
||||
deploy_maps_service() {
|
||||
log_info "Deploying maps-service..."
|
||||
|
||||
# Create a local overlay for development
|
||||
local temp_dir=$(mktemp -d)
|
||||
trap "rm -rf ${temp_dir}" EXIT
|
||||
|
||||
cp -r "${INFRA_PATH}/applications/maps-services/base" "${temp_dir}/"
|
||||
|
||||
# Create a custom kustomization for local development
|
||||
cat > "${temp_dir}/base/kustomization.yaml" <<EOF
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
commonLabels:
|
||||
service: maps-service
|
||||
|
||||
resources:
|
||||
- api.yaml
|
||||
- configmap.yaml
|
||||
- frontend.yaml
|
||||
- validator.yaml
|
||||
|
||||
images:
|
||||
- name: registry.itzana.me/strafesnet/maptest-api
|
||||
newName: localhost:${REGISTRY_PORT}/maptest-api
|
||||
newTag: local
|
||||
- name: registry.itzana.me/strafesnet/maptest-frontend
|
||||
newName: localhost:${REGISTRY_PORT}/maptest-frontend
|
||||
newTag: local
|
||||
- name: registry.itzana.me/strafesnet/maptest-validator
|
||||
newName: localhost:${REGISTRY_PORT}/maptest-validator
|
||||
newTag: local
|
||||
|
||||
patches:
|
||||
- target:
|
||||
kind: Deployment
|
||||
patch: |-
|
||||
- op: remove
|
||||
path: /spec/template/spec/imagePullSecrets
|
||||
EOF
|
||||
|
||||
kubectl apply -k "${temp_dir}/base" || {
|
||||
log_error "Failed to deploy maps-service"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_info "Waiting for maps-service to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l app=maptest-api --timeout=120s || log_warn "API not ready yet"
|
||||
kubectl wait --for=condition=ready pod -l app=maptest-frontend --timeout=120s || log_warn "Frontend not ready yet"
|
||||
kubectl wait --for=condition=ready pod -l app=maptest-validator --timeout=120s || log_warn "Validator not ready yet"
|
||||
}
|
||||
|
||||
# Port forwarding
|
||||
setup_port_forwarding() {
|
||||
log_info "Setting up port forwarding..."
|
||||
|
||||
log_info "Port forwarding for API (8080)..."
|
||||
kubectl port-forward svc/maptest-api 8080:8080 &
|
||||
|
||||
log_info "Port forwarding for Frontend (3000)..."
|
||||
kubectl port-forward svc/maptest-frontend 3000:3000 &
|
||||
|
||||
log_info "Port forwarding setup complete"
|
||||
log_info "You may need to manually manage these port-forwards or run them in separate terminals"
|
||||
}
|
||||
|
||||
# Display cluster info
|
||||
display_info() {
|
||||
log_info "======================================"
|
||||
log_info "KIND Cluster Setup Complete!"
|
||||
log_info "======================================"
|
||||
echo
|
||||
log_info "Cluster name: ${CLUSTER_NAME}"
|
||||
log_info "Local registry: localhost:${REGISTRY_PORT}"
|
||||
echo
|
||||
log_info "Services:"
|
||||
kubectl get svc
|
||||
echo
|
||||
log_info "Pods:"
|
||||
kubectl get pods
|
||||
echo
|
||||
log_info "Access your application:"
|
||||
log_info " - Frontend: http://localhost:3000"
|
||||
log_info " - API: http://localhost:8080"
|
||||
echo
|
||||
log_info "Useful commands:"
|
||||
log_info " - View logs: kubectl logs -f <pod-name>"
|
||||
log_info " - Get pods: kubectl get pods"
|
||||
log_info " - Delete cluster: kind delete cluster --name ${CLUSTER_NAME}"
|
||||
log_info " - Rebuild and redeploy: ./kind-setup.sh --rebuild"
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
log_info "Cleaning up..."
|
||||
kind delete cluster --name "${CLUSTER_NAME}"
|
||||
docker stop ${REGISTRY_NAME} && docker rm ${REGISTRY_NAME}
|
||||
log_info "Cleanup complete"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
local rebuild=false
|
||||
local cleanup_only=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--rebuild)
|
||||
rebuild=true
|
||||
shift
|
||||
;;
|
||||
--cleanup)
|
||||
cleanup_only=true
|
||||
shift
|
||||
;;
|
||||
--infra-path)
|
||||
INFRA_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo "Options:"
|
||||
echo " --rebuild Rebuild and push Docker images"
|
||||
echo " --cleanup Delete the cluster and registry"
|
||||
echo " --infra-path PATH Path to infra directory (default: ~/Documents/Projects/infra)"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$cleanup_only" = true ]; then
|
||||
cleanup
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate infra path
|
||||
if [ ! -d "$INFRA_PATH" ]; then
|
||||
log_error "Infra path does not exist: $INFRA_PATH"
|
||||
log_error "Please provide a valid path using --infra-path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$INFRA_PATH/applications/maps-services" ]; then
|
||||
log_error "maps-services not found in infra path: $INFRA_PATH/applications/maps-services"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Using infra path: $INFRA_PATH"
|
||||
|
||||
check_dependencies
|
||||
create_registry
|
||||
create_cluster
|
||||
|
||||
if [ "$rebuild" = true ]; then
|
||||
build_images
|
||||
fi
|
||||
|
||||
create_secrets
|
||||
deploy_dependencies
|
||||
deploy_maps_service
|
||||
display_info
|
||||
|
||||
log_info "Setup complete! Press Ctrl+C to stop port forwarding and exit."
|
||||
log_warn "Note: You may want to set up port-forwarding manually in separate terminals:"
|
||||
log_info " kubectl port-forward svc/maptest-api 8080:8080"
|
||||
log_info " kubectl port-forward svc/maptest-frontend 3000:3000"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
600
openapi.yaml
600
openapi.yaml
@@ -6,6 +6,8 @@ info:
|
||||
servers:
|
||||
- url: https://submissions.strafes.net/v1
|
||||
tags:
|
||||
- name: AOR
|
||||
description: AOR (Accept or Reject) event operations
|
||||
- name: Mapfixes
|
||||
description: Mapfix operations
|
||||
- name: Maps
|
||||
@@ -14,15 +16,41 @@ tags:
|
||||
description: Long-running operations
|
||||
- name: Session
|
||||
description: Session queries
|
||||
- name: Stats
|
||||
description: Statistics queries
|
||||
- name: Submissions
|
||||
description: Submission operations
|
||||
- name: Scripts
|
||||
description: Script operations
|
||||
- name: ScriptPolicy
|
||||
description: Script policy operations
|
||||
- name: Thumbnails
|
||||
description: Thumbnail operations
|
||||
- name: Users
|
||||
description: User operations
|
||||
security:
|
||||
- cookieAuth: []
|
||||
paths:
|
||||
/stats:
|
||||
get:
|
||||
summary: Get aggregate statistics
|
||||
operationId: getStats
|
||||
tags:
|
||||
- Stats
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Stats"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/session/user:
|
||||
get:
|
||||
summary: Get information about the currently logged in user
|
||||
@@ -421,6 +449,30 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/mapfixes/{MapfixID}/description:
|
||||
patch:
|
||||
summary: Update description (submitter only)
|
||||
operationId: updateMapfixDescription
|
||||
tags:
|
||||
- Mapfixes
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/MapfixID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 256
|
||||
responses:
|
||||
"204":
|
||||
description: Successful response
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/mapfixes/{MapfixID}/completed:
|
||||
post:
|
||||
summary: Called by maptest when a player completes the map
|
||||
@@ -910,6 +962,89 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/submissions/{SubmissionID}/reviews:
|
||||
get:
|
||||
summary: Get all reviews for a submission
|
||||
operationId: listSubmissionReviews
|
||||
tags:
|
||||
- Submissions
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SubmissionID'
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/SubmissionReview"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
post:
|
||||
summary: Create a review for a submission
|
||||
operationId: createSubmissionReview
|
||||
tags:
|
||||
- Submissions
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SubmissionID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SubmissionReviewCreate"
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SubmissionReview"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/submissions/{SubmissionID}/reviews/{ReviewID}:
|
||||
patch:
|
||||
summary: Update an existing review
|
||||
operationId: updateSubmissionReview
|
||||
tags:
|
||||
- Submissions
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SubmissionID'
|
||||
- name: ReviewID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SubmissionReviewCreate"
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SubmissionReview"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/submissions/{SubmissionID}/model:
|
||||
post:
|
||||
summary: Update model following role restrictions
|
||||
@@ -1174,6 +1309,109 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/aor-events:
|
||||
get:
|
||||
summary: Get list of AOR events
|
||||
operationId: listAOREvents
|
||||
tags:
|
||||
- AOR
|
||||
security: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AOREvent"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/aor-events/active:
|
||||
get:
|
||||
summary: Get the currently active AOR event
|
||||
operationId: getActiveAOREvent
|
||||
tags:
|
||||
- AOR
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AOREvent"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/aor-events/{AOREventID}:
|
||||
get:
|
||||
summary: Get a specific AOR event
|
||||
operationId: getAOREvent
|
||||
tags:
|
||||
- AOR
|
||||
security: []
|
||||
parameters:
|
||||
- name: AOREventID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 1
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AOREvent"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/aor-events/{AOREventID}/submissions:
|
||||
get:
|
||||
summary: Get all submissions for a specific AOR event
|
||||
operationId: getAOREventSubmissions
|
||||
tags:
|
||||
- AOR
|
||||
security: []
|
||||
parameters:
|
||||
- name: AOREventID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 1
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Submission"
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/script-policy:
|
||||
get:
|
||||
summary: Get list of script policies
|
||||
@@ -1438,6 +1676,222 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/thumbnails/assets:
|
||||
post:
|
||||
summary: Batch fetch asset thumbnails
|
||||
operationId: batchAssetThumbnails
|
||||
tags:
|
||||
- Thumbnails
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- assetIds
|
||||
properties:
|
||||
assetIds:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: uint64
|
||||
maxItems: 100
|
||||
description: Array of asset IDs (max 100)
|
||||
size:
|
||||
type: string
|
||||
enum:
|
||||
- "150x150"
|
||||
- "420x420"
|
||||
- "768x432"
|
||||
default: "420x420"
|
||||
description: Thumbnail size
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
thumbnails:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Map of asset ID to thumbnail URL
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/thumbnails/asset/{AssetID}:
|
||||
get:
|
||||
summary: Get single asset thumbnail
|
||||
operationId: getAssetThumbnail
|
||||
tags:
|
||||
- Thumbnails
|
||||
security: []
|
||||
parameters:
|
||||
- name: AssetID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: uint64
|
||||
- name: size
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- "150x150"
|
||||
- "420x420"
|
||||
- "768x432"
|
||||
default: "420x420"
|
||||
responses:
|
||||
"302":
|
||||
description: Redirect to thumbnail URL
|
||||
headers:
|
||||
Location:
|
||||
description: URL to redirect to
|
||||
schema:
|
||||
type: string
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/thumbnails/users:
|
||||
post:
|
||||
summary: Batch fetch user avatar thumbnails
|
||||
operationId: batchUserThumbnails
|
||||
tags:
|
||||
- Thumbnails
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- userIds
|
||||
properties:
|
||||
userIds:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: uint64
|
||||
maxItems: 100
|
||||
description: Array of user IDs (max 100)
|
||||
size:
|
||||
type: string
|
||||
enum:
|
||||
- "150x150"
|
||||
- "420x420"
|
||||
- "768x432"
|
||||
default: "150x150"
|
||||
description: Thumbnail size
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
thumbnails:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Map of user ID to thumbnail URL
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/thumbnails/user/{UserID}:
|
||||
get:
|
||||
summary: Get single user avatar thumbnail
|
||||
operationId: getUserThumbnail
|
||||
tags:
|
||||
- Thumbnails
|
||||
security: []
|
||||
parameters:
|
||||
- name: UserID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: uint64
|
||||
- name: size
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- "150x150"
|
||||
- "420x420"
|
||||
- "768x432"
|
||||
default: "150x150"
|
||||
responses:
|
||||
"302":
|
||||
description: Redirect to thumbnail URL
|
||||
headers:
|
||||
Location:
|
||||
description: URL to redirect to
|
||||
schema:
|
||||
type: string
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/usernames:
|
||||
post:
|
||||
summary: Batch fetch usernames
|
||||
operationId: batchUsernames
|
||||
tags:
|
||||
- Users
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- userIds
|
||||
properties:
|
||||
userIds:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: uint64
|
||||
maxItems: 100
|
||||
description: Array of user IDs (max 100)
|
||||
responses:
|
||||
"200":
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
usernames:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Map of user ID to username
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
components:
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
@@ -1517,6 +1971,56 @@ components:
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
schemas:
|
||||
AOREvent:
|
||||
type: object
|
||||
required:
|
||||
- ID
|
||||
- StartDate
|
||||
- FreezeDate
|
||||
- SelectionDate
|
||||
- DecisionDate
|
||||
- Status
|
||||
- CreatedAt
|
||||
- UpdatedAt
|
||||
properties:
|
||||
ID:
|
||||
type: integer
|
||||
format: int64
|
||||
StartDate:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Unix timestamp for the 1st day of AOR month
|
||||
FreezeDate:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Unix timestamp when submissions are frozen
|
||||
SelectionDate:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Unix timestamp when automatic selection occurs (end of week 1)
|
||||
DecisionDate:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Unix timestamp when final accept/reject decisions are made (end of month)
|
||||
Status:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
maximum: 5
|
||||
description: >
|
||||
AOR Event Status:
|
||||
* `0` - Scheduled
|
||||
* `1` - Open
|
||||
* `2` - Frozen
|
||||
* `3` - Selected
|
||||
* `4` - Completed
|
||||
* `5` - Closed
|
||||
CreatedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
UpdatedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
AuditEvent:
|
||||
type: object
|
||||
required:
|
||||
@@ -2061,6 +2565,102 @@ components:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
Stats:
|
||||
description: Aggregate statistics for submissions and mapfixes
|
||||
type: object
|
||||
properties:
|
||||
TotalSubmissions:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
description: Total number of submissions
|
||||
TotalMapfixes:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
description: Total number of mapfixes
|
||||
ReleasedSubmissions:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
description: Number of released submissions
|
||||
ReleasedMapfixes:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
description: Number of released mapfixes
|
||||
SubmittedSubmissions:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
description: Number of submissions under review
|
||||
SubmittedMapfixes:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
description: Number of mapfixes under review
|
||||
required:
|
||||
- TotalSubmissions
|
||||
- TotalMapfixes
|
||||
- ReleasedSubmissions
|
||||
- ReleasedMapfixes
|
||||
- SubmittedSubmissions
|
||||
- SubmittedMapfixes
|
||||
SubmissionReview:
|
||||
required:
|
||||
- ID
|
||||
- SubmissionID
|
||||
- ReviewerID
|
||||
- Recommend
|
||||
- Description
|
||||
- Outdated
|
||||
- CreatedAt
|
||||
- UpdatedAt
|
||||
type: object
|
||||
properties:
|
||||
ID:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
SubmissionID:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
ReviewerID:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
Recommend:
|
||||
type: boolean
|
||||
description: Whether the reviewer recommends accepting the submission
|
||||
Description:
|
||||
type: string
|
||||
maxLength: 2048
|
||||
description: Text description of the review reasoning
|
||||
Outdated:
|
||||
type: boolean
|
||||
description: Flag indicating if the review is outdated due to submission changes
|
||||
CreatedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
UpdatedAt:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
SubmissionReviewCreate:
|
||||
required:
|
||||
- Recommend
|
||||
- Description
|
||||
type: object
|
||||
properties:
|
||||
Recommend:
|
||||
type: boolean
|
||||
description: Whether the reviewer recommends accepting the submission
|
||||
Description:
|
||||
type: string
|
||||
maxLength: 2048
|
||||
description: Text description of the review reasoning
|
||||
Error:
|
||||
description: Represents error object
|
||||
type: object
|
||||
|
||||
@@ -5,14 +5,14 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
"github.com/ogen-go/ogen/middleware"
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/otelogen"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -32,6 +32,7 @@ type otelConfig struct {
|
||||
Tracer trace.Tracer
|
||||
MeterProvider metric.MeterProvider
|
||||
Meter metric.Meter
|
||||
Attributes []attribute.KeyValue
|
||||
}
|
||||
|
||||
func (cfg *otelConfig) initOTEL() {
|
||||
@@ -215,6 +216,13 @@ func WithMeterProvider(provider metric.MeterProvider) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// WithAttributes specifies default otel attributes.
|
||||
func WithAttributes(attributes ...attribute.KeyValue) Option {
|
||||
return otelOptionFunc(func(cfg *otelConfig) {
|
||||
cfg.Attributes = attributes
|
||||
})
|
||||
}
|
||||
|
||||
// WithClient specifies http client to use.
|
||||
func WithClient(client ht.Client) ClientOption {
|
||||
return optionFunc[clientConfig](func(cfg *clientConfig) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
19
pkg/api/oas_defaults_gen.go
Normal file
19
pkg/api/oas_defaults_gen.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Code generated by ogen, DO NOT EDIT.
|
||||
|
||||
package api
|
||||
|
||||
// setDefaults set default value of fields.
|
||||
func (s *BatchAssetThumbnailsReq) setDefaults() {
|
||||
{
|
||||
val := BatchAssetThumbnailsReqSize("420x420")
|
||||
s.Size.SetTo(val)
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaults set default value of fields.
|
||||
func (s *BatchUserThumbnailsReq) setDefaults() {
|
||||
{
|
||||
val := BatchUserThumbnailsReqSize("150x150")
|
||||
s.Size.SetTo(val)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,9 @@ const (
|
||||
ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload"
|
||||
ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate"
|
||||
ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated"
|
||||
BatchAssetThumbnailsOperation OperationName = "BatchAssetThumbnails"
|
||||
BatchUserThumbnailsOperation OperationName = "BatchUserThumbnails"
|
||||
BatchUsernamesOperation OperationName = "BatchUsernames"
|
||||
CreateMapfixOperation OperationName = "CreateMapfix"
|
||||
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
|
||||
CreateScriptOperation OperationName = "CreateScript"
|
||||
@@ -37,21 +40,30 @@ const (
|
||||
CreateSubmissionOperation OperationName = "CreateSubmission"
|
||||
CreateSubmissionAdminOperation OperationName = "CreateSubmissionAdmin"
|
||||
CreateSubmissionAuditCommentOperation OperationName = "CreateSubmissionAuditComment"
|
||||
CreateSubmissionReviewOperation OperationName = "CreateSubmissionReview"
|
||||
DeleteScriptOperation OperationName = "DeleteScript"
|
||||
DeleteScriptPolicyOperation OperationName = "DeleteScriptPolicy"
|
||||
DownloadMapAssetOperation OperationName = "DownloadMapAsset"
|
||||
GetAOREventOperation OperationName = "GetAOREvent"
|
||||
GetAOREventSubmissionsOperation OperationName = "GetAOREventSubmissions"
|
||||
GetActiveAOREventOperation OperationName = "GetActiveAOREvent"
|
||||
GetAssetThumbnailOperation OperationName = "GetAssetThumbnail"
|
||||
GetMapOperation OperationName = "GetMap"
|
||||
GetMapfixOperation OperationName = "GetMapfix"
|
||||
GetOperationOperation OperationName = "GetOperation"
|
||||
GetScriptOperation OperationName = "GetScript"
|
||||
GetScriptPolicyOperation OperationName = "GetScriptPolicy"
|
||||
GetStatsOperation OperationName = "GetStats"
|
||||
GetSubmissionOperation OperationName = "GetSubmission"
|
||||
GetUserThumbnailOperation OperationName = "GetUserThumbnail"
|
||||
ListAOREventsOperation OperationName = "ListAOREvents"
|
||||
ListMapfixAuditEventsOperation OperationName = "ListMapfixAuditEvents"
|
||||
ListMapfixesOperation OperationName = "ListMapfixes"
|
||||
ListMapsOperation OperationName = "ListMaps"
|
||||
ListScriptPolicyOperation OperationName = "ListScriptPolicy"
|
||||
ListScriptsOperation OperationName = "ListScripts"
|
||||
ListSubmissionAuditEventsOperation OperationName = "ListSubmissionAuditEvents"
|
||||
ListSubmissionReviewsOperation OperationName = "ListSubmissionReviews"
|
||||
ListSubmissionsOperation OperationName = "ListSubmissions"
|
||||
ReleaseSubmissionsOperation OperationName = "ReleaseSubmissions"
|
||||
SessionRolesOperation OperationName = "SessionRoles"
|
||||
@@ -59,8 +71,10 @@ const (
|
||||
SessionValidateOperation OperationName = "SessionValidate"
|
||||
SetMapfixCompletedOperation OperationName = "SetMapfixCompleted"
|
||||
SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted"
|
||||
UpdateMapfixDescriptionOperation OperationName = "UpdateMapfixDescription"
|
||||
UpdateMapfixModelOperation OperationName = "UpdateMapfixModel"
|
||||
UpdateScriptOperation OperationName = "UpdateScript"
|
||||
UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy"
|
||||
UpdateSubmissionModelOperation OperationName = "UpdateSubmissionModel"
|
||||
UpdateSubmissionReviewOperation OperationName = "UpdateSubmissionReview"
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
@@ -10,13 +11,13 @@ import (
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
)
|
||||
|
||||
func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
req *MapfixTriggerCreate,
|
||||
func (s *Server) decodeBatchAssetThumbnailsRequest(r *http.Request) (
|
||||
req *BatchAssetThumbnailsReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -37,22 +38,266 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request BatchAssetThumbnailsReq
|
||||
if err := func() error {
|
||||
if err := request.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 req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeBatchUserThumbnailsRequest(r *http.Request) (
|
||||
req *BatchUserThumbnailsReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request BatchUserThumbnailsReq
|
||||
if err := func() error {
|
||||
if err := request.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 req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeBatchUsernamesRequest(r *http.Request) (
|
||||
req *BatchUsernamesReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request BatchUsernamesReq
|
||||
if err := func() error {
|
||||
if err := request.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 req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
req *MapfixTriggerCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request MapfixTriggerCreate
|
||||
@@ -70,7 +315,7 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -78,16 +323,17 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, close, nil
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
||||
req CreateMapfixAuditCommentReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -108,20 +354,21 @@ func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "text/plain":
|
||||
reader := r.Body
|
||||
request := CreateMapfixAuditCommentReq{Data: reader}
|
||||
return request, close, nil
|
||||
return request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
req *ScriptCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -142,22 +389,29 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptCreate
|
||||
@@ -175,7 +429,7 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -183,16 +437,17 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, close, nil
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
req *ScriptPolicyCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -213,22 +468,29 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptPolicyCreate
|
||||
@@ -246,7 +508,7 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -254,16 +516,17 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, close, nil
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
req *SubmissionTriggerCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -284,22 +547,29 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SubmissionTriggerCreate
|
||||
@@ -317,7 +587,7 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -325,16 +595,17 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, close, nil
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
req *SubmissionTriggerCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -355,22 +626,29 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SubmissionTriggerCreate
|
||||
@@ -388,7 +666,7 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -396,16 +674,17 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, close, nil
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
||||
req CreateSubmissionAuditCommentReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -426,20 +705,21 @@ func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "text/plain":
|
||||
reader := r.Body
|
||||
request := CreateSubmissionAuditCommentReq{Data: reader}
|
||||
return request, close, nil
|
||||
return request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
req []ReleaseInfo,
|
||||
func (s *Server) decodeCreateSubmissionReviewRequest(r *http.Request) (
|
||||
req *SubmissionReviewCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -460,22 +740,108 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SubmissionReviewCreate
|
||||
if err := func() error {
|
||||
if err := request.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 req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
req []ReleaseInfo,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request []ReleaseInfo
|
||||
@@ -501,7 +867,7 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if request == nil {
|
||||
@@ -534,16 +900,17 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return request, close, nil
|
||||
return request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
req *ScriptUpdate,
|
||||
func (s *Server) decodeUpdateMapfixDescriptionRequest(r *http.Request) (
|
||||
req UpdateMapfixDescriptionReq,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -564,22 +931,64 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "text/plain":
|
||||
reader := r.Body
|
||||
request := UpdateMapfixDescriptionReq{Data: reader}
|
||||
return request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
req *ScriptUpdate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptUpdate
|
||||
@@ -597,7 +1006,7 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -605,16 +1014,17 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, close, nil
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
req *ScriptPolicyUpdate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
@@ -635,22 +1045,29 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, close, errors.Wrap(err, "parse media type")
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, close, validate.ErrBodyRequired
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request ScriptPolicyUpdate
|
||||
@@ -668,7 +1085,7 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return req, close, err
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
@@ -676,10 +1093,89 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, close, errors.Wrap(err, "validate")
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, close, nil
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, close, validate.InvalidContentType(ct)
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeUpdateSubmissionReviewRequest(r *http.Request) (
|
||||
req *SubmissionReviewCreate,
|
||||
rawBody []byte,
|
||||
close func() error,
|
||||
rerr error,
|
||||
) {
|
||||
var closers []func() error
|
||||
close = func() error {
|
||||
var merr error
|
||||
// Close in reverse order, to match defer behavior.
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
c := closers[i]
|
||||
merr = errors.Join(merr, c())
|
||||
}
|
||||
return merr
|
||||
}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
rerr = errors.Join(rerr, close())
|
||||
}
|
||||
}()
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
if r.ContentLength == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
buf, err := io.ReadAll(r.Body)
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
if err != nil {
|
||||
return req, rawBody, close, err
|
||||
}
|
||||
|
||||
// Reset the body to allow for downstream reading.
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(buf))
|
||||
|
||||
if len(buf) == 0 {
|
||||
return req, rawBody, close, validate.ErrBodyRequired
|
||||
}
|
||||
|
||||
rawBody = append(rawBody, buf...)
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var request SubmissionReviewCreate
|
||||
if err := func() error {
|
||||
if err := request.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 req, rawBody, close, err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := request.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return req, rawBody, close, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &request, rawBody, close, nil
|
||||
default:
|
||||
return req, rawBody, close, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,51 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-faster/jx"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
)
|
||||
|
||||
func encodeBatchAssetThumbnailsRequest(
|
||||
req *BatchAssetThumbnailsReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeBatchUserThumbnailsRequest(
|
||||
req *BatchUserThumbnailsReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeBatchUsernamesRequest(
|
||||
req *BatchUsernamesReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateMapfixRequest(
|
||||
req *MapfixTriggerCreate,
|
||||
r *http.Request,
|
||||
@@ -101,6 +142,20 @@ func encodeCreateSubmissionAuditCommentRequest(
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateSubmissionReviewRequest(
|
||||
req *SubmissionReviewCreate,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeReleaseSubmissionsRequest(
|
||||
req []ReleaseInfo,
|
||||
r *http.Request,
|
||||
@@ -119,6 +174,16 @@ func encodeReleaseSubmissionsRequest(
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeUpdateMapfixDescriptionRequest(
|
||||
req UpdateMapfixDescriptionReq,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "text/plain"
|
||||
body := req
|
||||
ht.SetBody(r, body, contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeUpdateScriptRequest(
|
||||
req *ScriptUpdate,
|
||||
r *http.Request,
|
||||
@@ -146,3 +211,17 @@ func encodeUpdateScriptPolicyRequest(
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeUpdateSubmissionReviewRequest(
|
||||
req *SubmissionReviewCreate,
|
||||
r *http.Request,
|
||||
) error {
|
||||
const contentType = "application/json"
|
||||
e := new(jx.Encoder)
|
||||
{
|
||||
req.Encode(e)
|
||||
}
|
||||
encoded := e.Bytes()
|
||||
ht.SetBody(r, bytes.NewReader(encoded), contentType)
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,11 @@ import (
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
"github.com/go-faster/jx"
|
||||
"github.com/ogen-go/ogen/conv"
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
"github.com/ogen-go/ogen/uri"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
)
|
||||
|
||||
func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
@@ -182,6 +183,48 @@ func encodeActionSubmissionValidatedResponse(response *ActionSubmissionValidated
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeBatchAssetThumbnailsResponse(response *BatchAssetThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeBatchUserThumbnailsResponse(response *BatchUserThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeBatchUsernamesResponse(response *BatchUsernamesOK, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateMapfixResponse(response *OperationID, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(201)
|
||||
@@ -266,6 +309,20 @@ func encodeCreateSubmissionAuditCommentResponse(response *CreateSubmissionAuditC
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCreateSubmissionReviewResponse(response *SubmissionReview, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeDeleteScriptResponse(response *DeleteScriptNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
@@ -296,6 +353,78 @@ func encodeDownloadMapAssetResponse(response DownloadMapAssetOK, w http.Response
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetAOREventResponse(response *AOREvent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetAOREventSubmissionsResponse(response []Submission, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
e.ArrStart()
|
||||
for _, elem := range response {
|
||||
elem.Encode(e)
|
||||
}
|
||||
e.ArrEnd()
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetActiveAOREventResponse(response *AOREvent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetAssetThumbnailResponse(response *GetAssetThumbnailFound, w http.ResponseWriter, span trace.Span) error {
|
||||
// Encoding response headers.
|
||||
{
|
||||
h := uri.NewHeaderEncoder(w.Header())
|
||||
// Encode "Location" header.
|
||||
{
|
||||
cfg := uri.HeaderParameterEncodingConfig{
|
||||
Name: "Location",
|
||||
Explode: false,
|
||||
}
|
||||
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
|
||||
if val, ok := response.Location.Get(); ok {
|
||||
return e.EncodeValue(conv.StringToString(val))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "encode Location header")
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(302)
|
||||
span.SetStatus(codes.Ok, http.StatusText(302))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetMapResponse(response *Map, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -366,6 +495,20 @@ func encodeGetScriptPolicyResponse(response *ScriptPolicy, w http.ResponseWriter
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetStatsResponse(response *Stats, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -380,6 +523,50 @@ func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, sp
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeGetUserThumbnailResponse(response *GetUserThumbnailFound, w http.ResponseWriter, span trace.Span) error {
|
||||
// Encoding response headers.
|
||||
{
|
||||
h := uri.NewHeaderEncoder(w.Header())
|
||||
// Encode "Location" header.
|
||||
{
|
||||
cfg := uri.HeaderParameterEncodingConfig{
|
||||
Name: "Location",
|
||||
Explode: false,
|
||||
}
|
||||
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
|
||||
if val, ok := response.Location.Get(); ok {
|
||||
return e.EncodeValue(conv.StringToString(val))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "encode Location header")
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(302)
|
||||
span.SetStatus(codes.Ok, http.StatusText(302))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListAOREventsResponse(response []AOREvent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
e.ArrStart()
|
||||
for _, elem := range response {
|
||||
elem.Encode(e)
|
||||
}
|
||||
e.ArrEnd()
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListMapfixAuditEventsResponse(response []AuditEvent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -484,6 +671,24 @@ func encodeListSubmissionAuditEventsResponse(response []AuditEvent, w http.Respo
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListSubmissionReviewsResponse(response []SubmissionReview, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
e.ArrStart()
|
||||
for _, elem := range response {
|
||||
elem.Encode(e)
|
||||
}
|
||||
e.ArrEnd()
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeListSubmissionsResponse(response *Submissions, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -568,6 +773,13 @@ func encodeSetSubmissionCompletedResponse(response *SetSubmissionCompletedNoCont
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeUpdateMapfixDescriptionResponse(response *UpdateMapfixDescriptionNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeUpdateMapfixModelResponse(response *UpdateMapfixModelNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
@@ -596,6 +808,20 @@ func encodeUpdateSubmissionModelResponse(response *UpdateSubmissionModelNoConten
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeUpdateSubmissionReviewResponse(response *SubmissionReview, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
span.SetStatus(codes.Ok, http.StatusText(200))
|
||||
|
||||
e := new(jx.Encoder)
|
||||
response.Encode(e)
|
||||
if _, err := e.WriteTo(w); err != nil {
|
||||
return errors.Wrap(err, "write")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeErrorResponse(response *ErrorStatusCode, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
code := response.StatusCode
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-faster/errors"
|
||||
|
||||
"github.com/ogen-go/ogen/ogenerrors"
|
||||
)
|
||||
|
||||
@@ -65,20 +64,24 @@ var operationRolesCookieAuth = map[string][]string{
|
||||
CreateSubmissionOperation: []string{},
|
||||
CreateSubmissionAdminOperation: []string{},
|
||||
CreateSubmissionAuditCommentOperation: []string{},
|
||||
CreateSubmissionReviewOperation: []string{},
|
||||
DeleteScriptOperation: []string{},
|
||||
DeleteScriptPolicyOperation: []string{},
|
||||
DownloadMapAssetOperation: []string{},
|
||||
GetOperationOperation: []string{},
|
||||
ListSubmissionReviewsOperation: []string{},
|
||||
ReleaseSubmissionsOperation: []string{},
|
||||
SessionRolesOperation: []string{},
|
||||
SessionUserOperation: []string{},
|
||||
SessionValidateOperation: []string{},
|
||||
SetMapfixCompletedOperation: []string{},
|
||||
SetSubmissionCompletedOperation: []string{},
|
||||
UpdateMapfixDescriptionOperation: []string{},
|
||||
UpdateMapfixModelOperation: []string{},
|
||||
UpdateScriptOperation: []string{},
|
||||
UpdateScriptPolicyOperation: []string{},
|
||||
UpdateSubmissionModelOperation: []string{},
|
||||
UpdateSubmissionReviewOperation: []string{},
|
||||
}
|
||||
|
||||
func (s *Server) securityCookieAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
|
||||
|
||||
@@ -155,6 +155,24 @@ type Handler interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/status/reset-uploading
|
||||
ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error
|
||||
// BatchAssetThumbnails implements batchAssetThumbnails operation.
|
||||
//
|
||||
// Batch fetch asset thumbnails.
|
||||
//
|
||||
// POST /thumbnails/assets
|
||||
BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (*BatchAssetThumbnailsOK, error)
|
||||
// BatchUserThumbnails implements batchUserThumbnails operation.
|
||||
//
|
||||
// Batch fetch user avatar thumbnails.
|
||||
//
|
||||
// POST /thumbnails/users
|
||||
BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (*BatchUserThumbnailsOK, error)
|
||||
// BatchUsernames implements batchUsernames operation.
|
||||
//
|
||||
// Batch fetch usernames.
|
||||
//
|
||||
// POST /usernames
|
||||
BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (*BatchUsernamesOK, error)
|
||||
// CreateMapfix implements createMapfix operation.
|
||||
//
|
||||
// Trigger the validator to create a mapfix.
|
||||
@@ -197,6 +215,12 @@ type Handler interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/comment
|
||||
CreateSubmissionAuditComment(ctx context.Context, req CreateSubmissionAuditCommentReq, params CreateSubmissionAuditCommentParams) error
|
||||
// CreateSubmissionReview implements createSubmissionReview operation.
|
||||
//
|
||||
// Create a review for a submission.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/reviews
|
||||
CreateSubmissionReview(ctx context.Context, req *SubmissionReviewCreate, params CreateSubmissionReviewParams) (*SubmissionReview, error)
|
||||
// DeleteScript implements deleteScript operation.
|
||||
//
|
||||
// Delete the specified script by ID.
|
||||
@@ -215,6 +239,30 @@ type Handler interface {
|
||||
//
|
||||
// GET /maps/{MapID}/download
|
||||
DownloadMapAsset(ctx context.Context, params DownloadMapAssetParams) (DownloadMapAssetOK, error)
|
||||
// GetAOREvent implements getAOREvent operation.
|
||||
//
|
||||
// Get a specific AOR event.
|
||||
//
|
||||
// GET /aor-events/{AOREventID}
|
||||
GetAOREvent(ctx context.Context, params GetAOREventParams) (*AOREvent, error)
|
||||
// GetAOREventSubmissions implements getAOREventSubmissions operation.
|
||||
//
|
||||
// Get all submissions for a specific AOR event.
|
||||
//
|
||||
// GET /aor-events/{AOREventID}/submissions
|
||||
GetAOREventSubmissions(ctx context.Context, params GetAOREventSubmissionsParams) ([]Submission, error)
|
||||
// GetActiveAOREvent implements getActiveAOREvent operation.
|
||||
//
|
||||
// Get the currently active AOR event.
|
||||
//
|
||||
// GET /aor-events/active
|
||||
GetActiveAOREvent(ctx context.Context) (*AOREvent, error)
|
||||
// GetAssetThumbnail implements getAssetThumbnail operation.
|
||||
//
|
||||
// Get single asset thumbnail.
|
||||
//
|
||||
// GET /thumbnails/asset/{AssetID}
|
||||
GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (*GetAssetThumbnailFound, error)
|
||||
// GetMap implements getMap operation.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
@@ -245,12 +293,30 @@ type Handler interface {
|
||||
//
|
||||
// GET /script-policy/{ScriptPolicyID}
|
||||
GetScriptPolicy(ctx context.Context, params GetScriptPolicyParams) (*ScriptPolicy, error)
|
||||
// GetStats implements getStats operation.
|
||||
//
|
||||
// Get aggregate statistics.
|
||||
//
|
||||
// GET /stats
|
||||
GetStats(ctx context.Context) (*Stats, error)
|
||||
// GetSubmission implements getSubmission operation.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
//
|
||||
// GET /submissions/{SubmissionID}
|
||||
GetSubmission(ctx context.Context, params GetSubmissionParams) (*Submission, error)
|
||||
// GetUserThumbnail implements getUserThumbnail operation.
|
||||
//
|
||||
// Get single user avatar thumbnail.
|
||||
//
|
||||
// GET /thumbnails/user/{UserID}
|
||||
GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (*GetUserThumbnailFound, error)
|
||||
// ListAOREvents implements listAOREvents operation.
|
||||
//
|
||||
// Get list of AOR events.
|
||||
//
|
||||
// GET /aor-events
|
||||
ListAOREvents(ctx context.Context, params ListAOREventsParams) ([]AOREvent, error)
|
||||
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
|
||||
//
|
||||
// Retrieve a list of audit events.
|
||||
@@ -287,6 +353,12 @@ type Handler interface {
|
||||
//
|
||||
// GET /submissions/{SubmissionID}/audit-events
|
||||
ListSubmissionAuditEvents(ctx context.Context, params ListSubmissionAuditEventsParams) ([]AuditEvent, error)
|
||||
// ListSubmissionReviews implements listSubmissionReviews operation.
|
||||
//
|
||||
// Get all reviews for a submission.
|
||||
//
|
||||
// GET /submissions/{SubmissionID}/reviews
|
||||
ListSubmissionReviews(ctx context.Context, params ListSubmissionReviewsParams) ([]SubmissionReview, error)
|
||||
// ListSubmissions implements listSubmissions operation.
|
||||
//
|
||||
// Get list of submissions.
|
||||
@@ -329,6 +401,12 @@ type Handler interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/completed
|
||||
SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error
|
||||
// UpdateMapfixDescription implements updateMapfixDescription operation.
|
||||
//
|
||||
// Update description (submitter only).
|
||||
//
|
||||
// PATCH /mapfixes/{MapfixID}/description
|
||||
UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error
|
||||
// UpdateMapfixModel implements updateMapfixModel operation.
|
||||
//
|
||||
// Update model following role restrictions.
|
||||
@@ -353,6 +431,12 @@ type Handler interface {
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/model
|
||||
UpdateSubmissionModel(ctx context.Context, params UpdateSubmissionModelParams) error
|
||||
// UpdateSubmissionReview implements updateSubmissionReview operation.
|
||||
//
|
||||
// Update an existing review.
|
||||
//
|
||||
// PATCH /submissions/{SubmissionID}/reviews/{ReviewID}
|
||||
UpdateSubmissionReview(ctx context.Context, req *SubmissionReviewCreate, params UpdateSubmissionReviewParams) (*SubmissionReview, error)
|
||||
// NewError creates *ErrorStatusCode from error returned by handler.
|
||||
//
|
||||
// Used for common default response.
|
||||
|
||||
@@ -232,6 +232,33 @@ func (UnimplementedHandler) ActionSubmissionValidated(ctx context.Context, param
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// BatchAssetThumbnails implements batchAssetThumbnails operation.
|
||||
//
|
||||
// Batch fetch asset thumbnails.
|
||||
//
|
||||
// POST /thumbnails/assets
|
||||
func (UnimplementedHandler) BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (r *BatchAssetThumbnailsOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// BatchUserThumbnails implements batchUserThumbnails operation.
|
||||
//
|
||||
// Batch fetch user avatar thumbnails.
|
||||
//
|
||||
// POST /thumbnails/users
|
||||
func (UnimplementedHandler) BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (r *BatchUserThumbnailsOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// BatchUsernames implements batchUsernames operation.
|
||||
//
|
||||
// Batch fetch usernames.
|
||||
//
|
||||
// POST /usernames
|
||||
func (UnimplementedHandler) BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (r *BatchUsernamesOK, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CreateMapfix implements createMapfix operation.
|
||||
//
|
||||
// Trigger the validator to create a mapfix.
|
||||
@@ -295,6 +322,15 @@ func (UnimplementedHandler) CreateSubmissionAuditComment(ctx context.Context, re
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CreateSubmissionReview implements createSubmissionReview operation.
|
||||
//
|
||||
// Create a review for a submission.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/reviews
|
||||
func (UnimplementedHandler) CreateSubmissionReview(ctx context.Context, req *SubmissionReviewCreate, params CreateSubmissionReviewParams) (r *SubmissionReview, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// DeleteScript implements deleteScript operation.
|
||||
//
|
||||
// Delete the specified script by ID.
|
||||
@@ -322,6 +358,42 @@ func (UnimplementedHandler) DownloadMapAsset(ctx context.Context, params Downloa
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetAOREvent implements getAOREvent operation.
|
||||
//
|
||||
// Get a specific AOR event.
|
||||
//
|
||||
// GET /aor-events/{AOREventID}
|
||||
func (UnimplementedHandler) GetAOREvent(ctx context.Context, params GetAOREventParams) (r *AOREvent, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetAOREventSubmissions implements getAOREventSubmissions operation.
|
||||
//
|
||||
// Get all submissions for a specific AOR event.
|
||||
//
|
||||
// GET /aor-events/{AOREventID}/submissions
|
||||
func (UnimplementedHandler) GetAOREventSubmissions(ctx context.Context, params GetAOREventSubmissionsParams) (r []Submission, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetActiveAOREvent implements getActiveAOREvent operation.
|
||||
//
|
||||
// Get the currently active AOR event.
|
||||
//
|
||||
// GET /aor-events/active
|
||||
func (UnimplementedHandler) GetActiveAOREvent(ctx context.Context) (r *AOREvent, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetAssetThumbnail implements getAssetThumbnail operation.
|
||||
//
|
||||
// Get single asset thumbnail.
|
||||
//
|
||||
// GET /thumbnails/asset/{AssetID}
|
||||
func (UnimplementedHandler) GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (r *GetAssetThumbnailFound, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetMap implements getMap operation.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
@@ -367,6 +439,15 @@ func (UnimplementedHandler) GetScriptPolicy(ctx context.Context, params GetScrip
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetStats implements getStats operation.
|
||||
//
|
||||
// Get aggregate statistics.
|
||||
//
|
||||
// GET /stats
|
||||
func (UnimplementedHandler) GetStats(ctx context.Context) (r *Stats, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetSubmission implements getSubmission operation.
|
||||
//
|
||||
// Retrieve map with ID.
|
||||
@@ -376,6 +457,24 @@ func (UnimplementedHandler) GetSubmission(ctx context.Context, params GetSubmiss
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// GetUserThumbnail implements getUserThumbnail operation.
|
||||
//
|
||||
// Get single user avatar thumbnail.
|
||||
//
|
||||
// GET /thumbnails/user/{UserID}
|
||||
func (UnimplementedHandler) GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (r *GetUserThumbnailFound, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListAOREvents implements listAOREvents operation.
|
||||
//
|
||||
// Get list of AOR events.
|
||||
//
|
||||
// GET /aor-events
|
||||
func (UnimplementedHandler) ListAOREvents(ctx context.Context, params ListAOREventsParams) (r []AOREvent, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
|
||||
//
|
||||
// Retrieve a list of audit events.
|
||||
@@ -430,6 +529,15 @@ func (UnimplementedHandler) ListSubmissionAuditEvents(ctx context.Context, param
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListSubmissionReviews implements listSubmissionReviews operation.
|
||||
//
|
||||
// Get all reviews for a submission.
|
||||
//
|
||||
// GET /submissions/{SubmissionID}/reviews
|
||||
func (UnimplementedHandler) ListSubmissionReviews(ctx context.Context, params ListSubmissionReviewsParams) (r []SubmissionReview, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ListSubmissions implements listSubmissions operation.
|
||||
//
|
||||
// Get list of submissions.
|
||||
@@ -493,6 +601,15 @@ func (UnimplementedHandler) SetSubmissionCompleted(ctx context.Context, params S
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// UpdateMapfixDescription implements updateMapfixDescription operation.
|
||||
//
|
||||
// Update description (submitter only).
|
||||
//
|
||||
// PATCH /mapfixes/{MapfixID}/description
|
||||
func (UnimplementedHandler) UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// UpdateMapfixModel implements updateMapfixModel operation.
|
||||
//
|
||||
// Update model following role restrictions.
|
||||
@@ -529,6 +646,15 @@ func (UnimplementedHandler) UpdateSubmissionModel(ctx context.Context, params Up
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// UpdateSubmissionReview implements updateSubmissionReview operation.
|
||||
//
|
||||
// Update an existing review.
|
||||
//
|
||||
// PATCH /submissions/{SubmissionID}/reviews/{ReviewID}
|
||||
func (UnimplementedHandler) UpdateSubmissionReview(ctx context.Context, req *SubmissionReviewCreate, params UpdateSubmissionReviewParams) (r *SubmissionReview, _ error) {
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// NewError creates *ErrorStatusCode from error returned by handler.
|
||||
//
|
||||
// Used for common default response.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
75
pkg/cmds/aor.go
Normal file
75
pkg/cmds/aor.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore/gormstore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func NewAORCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "aor",
|
||||
Usage: "Run AOR (Accept or Reject) event processor",
|
||||
Action: runAORProcessor,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "pg-host",
|
||||
Usage: "Host of postgres database",
|
||||
EnvVars: []string{"PG_HOST"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "pg-port",
|
||||
Usage: "Port of postgres database",
|
||||
EnvVars: []string{"PG_PORT"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pg-db",
|
||||
Usage: "Name of database to connect to",
|
||||
EnvVars: []string{"PG_DB"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pg-user",
|
||||
Usage: "User to connect with",
|
||||
EnvVars: []string{"PG_USER"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pg-password",
|
||||
Usage: "Password to connect with",
|
||||
EnvVars: []string{"PG_PASSWORD"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "migrate",
|
||||
Usage: "Run database migrations",
|
||||
Value: false,
|
||||
EnvVars: []string{"MIGRATE"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runAORProcessor(ctx *cli.Context) error {
|
||||
log.Info("Starting AOR event processor")
|
||||
|
||||
// Connect to database
|
||||
db, err := gormstore.New(ctx)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("failed to connect database")
|
||||
return err
|
||||
}
|
||||
|
||||
// Create scheduler and process events
|
||||
scheduler := service.NewAORScheduler(db)
|
||||
if err := scheduler.ProcessAOREvents(); err != nil {
|
||||
log.WithError(err).Error("AOR event processing failed")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("AOR event processor completed successfully")
|
||||
return nil
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/validator_controller"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/web_api"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/redis/go-redis/v9"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
"google.golang.org/grpc"
|
||||
@@ -102,6 +103,24 @@ func NewServeCommand() *cli.Command {
|
||||
EnvVars: []string{"RBX_API_KEY"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "redis-host",
|
||||
Usage: "Host of Redis cache",
|
||||
EnvVars: []string{"REDIS_HOST"},
|
||||
Value: "localhost:6379",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "redis-password",
|
||||
Usage: "Password for Redis",
|
||||
EnvVars: []string{"REDIS_PASSWORD"},
|
||||
Value: "",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "redis-db",
|
||||
Usage: "Redis database number",
|
||||
EnvVars: []string{"REDIS_DB"},
|
||||
Value: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -129,6 +148,24 @@ func serve(ctx *cli.Context) error {
|
||||
log.WithError(err).Fatal("failed to add stream")
|
||||
}
|
||||
|
||||
// Initialize Redis client
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: ctx.String("redis-host"),
|
||||
Password: ctx.String("redis-password"),
|
||||
DB: ctx.Int("redis-db"),
|
||||
})
|
||||
|
||||
// Test Redis connection
|
||||
if err := redisClient.Ping(ctx.Context).Err(); err != nil {
|
||||
log.WithError(err).Warn("failed to connect to Redis - thumbnails will not be cached")
|
||||
}
|
||||
|
||||
// Initialize Roblox client
|
||||
robloxClient := &roblox.Client{
|
||||
HttpClient: http.DefaultClient,
|
||||
ApiKey: ctx.String("rbx-api-key"),
|
||||
}
|
||||
|
||||
// connect to main game database
|
||||
conn, err := grpc.Dial(ctx.String("data-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
@@ -139,13 +176,15 @@ func serve(ctx *cli.Context) error {
|
||||
js,
|
||||
maps.NewMapsServiceClient(conn),
|
||||
users.NewUsersServiceClient(conn),
|
||||
robloxClient,
|
||||
redisClient,
|
||||
)
|
||||
|
||||
svc_external := web_api.NewService(
|
||||
&svc_inner,
|
||||
roblox.Client{
|
||||
HttpClient: http.DefaultClient,
|
||||
ApiKey: ctx.String("rbx-api-key"),
|
||||
ApiKey: ctx.String("rbx-api-key"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -24,11 +24,14 @@ const (
|
||||
)
|
||||
|
||||
type Datastore interface {
|
||||
AOREvents() AOREvents
|
||||
AORSubmissions() AORSubmissions
|
||||
AuditEvents() AuditEvents
|
||||
Maps() Maps
|
||||
Mapfixes() Mapfixes
|
||||
Operations() Operations
|
||||
Submissions() Submissions
|
||||
SubmissionReviews() SubmissionReviews
|
||||
Scripts() Scripts
|
||||
ScriptPolicy() ScriptPolicy
|
||||
}
|
||||
@@ -83,6 +86,16 @@ type Submissions interface {
|
||||
ListWithTotal(ctx context.Context, filters OptionalMap, page model.Page, sort ListSort) (int64, []model.Submission, error)
|
||||
}
|
||||
|
||||
type SubmissionReviews interface {
|
||||
Get(ctx context.Context, id int64) (model.SubmissionReview, error)
|
||||
GetBySubmissionAndReviewer(ctx context.Context, submissionID int64, reviewerID uint64) (model.SubmissionReview, error)
|
||||
Create(ctx context.Context, review model.SubmissionReview) (model.SubmissionReview, error)
|
||||
Update(ctx context.Context, id int64, values OptionalMap) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
ListBySubmission(ctx context.Context, submissionID int64) ([]model.SubmissionReview, error)
|
||||
MarkOutdatedBySubmission(ctx context.Context, submissionID int64) error
|
||||
}
|
||||
|
||||
type Scripts interface {
|
||||
Get(ctx context.Context, id int64) (model.Script, error)
|
||||
Create(ctx context.Context, smap model.Script) (model.Script, error)
|
||||
@@ -99,3 +112,22 @@ type ScriptPolicy interface {
|
||||
Delete(ctx context.Context, id int64) error
|
||||
List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.ScriptPolicy, error)
|
||||
}
|
||||
|
||||
type AOREvents interface {
|
||||
Get(ctx context.Context, id int64) (model.AOREvent, error)
|
||||
GetActive(ctx context.Context) (model.AOREvent, error)
|
||||
GetByStatus(ctx context.Context, status model.AOREventStatus) ([]model.AOREvent, error)
|
||||
Create(ctx context.Context, event model.AOREvent) (model.AOREvent, error)
|
||||
Update(ctx context.Context, id int64, values OptionalMap) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.AOREvent, error)
|
||||
}
|
||||
|
||||
type AORSubmissions interface {
|
||||
Get(ctx context.Context, id int64) (model.AORSubmission, error)
|
||||
GetByAOREvent(ctx context.Context, eventID int64) ([]model.AORSubmission, error)
|
||||
GetBySubmission(ctx context.Context, submissionID int64) ([]model.AORSubmission, error)
|
||||
Create(ctx context.Context, aorSubmission model.AORSubmission) (model.AORSubmission, error)
|
||||
Delete(ctx context.Context, id int64) error
|
||||
ListWithSubmissions(ctx context.Context, eventID int64) ([]model.Submission, error)
|
||||
}
|
||||
|
||||
89
pkg/datastore/gormstore/aor_events.go
Normal file
89
pkg/datastore/gormstore/aor_events.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package gormstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AOREvents struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (env *AOREvents) Get(ctx context.Context, id int64) (model.AOREvent, error) {
|
||||
var event model.AOREvent
|
||||
if err := env.db.First(&event, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return event, datastore.ErrNotExist
|
||||
}
|
||||
return event, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (env *AOREvents) GetActive(ctx context.Context) (model.AOREvent, error) {
|
||||
var event model.AOREvent
|
||||
// Get the most recent non-closed event
|
||||
if err := env.db.Where("status != ?", model.AOREventStatusClosed).
|
||||
Order("start_date DESC").
|
||||
First(&event).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return event, datastore.ErrNotExist
|
||||
}
|
||||
return event, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (env *AOREvents) GetByStatus(ctx context.Context, status model.AOREventStatus) ([]model.AOREvent, error) {
|
||||
var events []model.AOREvent
|
||||
if err := env.db.Where("status = ?", status).Order("start_date DESC").Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (env *AOREvents) Create(ctx context.Context, event model.AOREvent) (model.AOREvent, error) {
|
||||
if err := env.db.Create(&event).Error; err != nil {
|
||||
return event, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (env *AOREvents) Update(ctx context.Context, id int64, values datastore.OptionalMap) error {
|
||||
if err := env.db.Model(&model.AOREvent{}).Where("id = ?", id).Updates(values.Map()).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return datastore.ErrNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (env *AOREvents) Delete(ctx context.Context, id int64) error {
|
||||
if err := env.db.Delete(&model.AOREvent{}, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return datastore.ErrNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (env *AOREvents) List(ctx context.Context, filters datastore.OptionalMap, page model.Page) ([]model.AOREvent, error) {
|
||||
var events []model.AOREvent
|
||||
query := env.db.Where(filters.Map())
|
||||
|
||||
if page.Size > 0 {
|
||||
offset := (page.Number - 1) * page.Size
|
||||
query = query.Limit(int(page.Size)).Offset(int(offset))
|
||||
}
|
||||
|
||||
if err := query.Order("start_date DESC").Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
70
pkg/datastore/gormstore/aor_submissions.go
Normal file
70
pkg/datastore/gormstore/aor_submissions.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package gormstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AORSubmissions struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (env *AORSubmissions) Get(ctx context.Context, id int64) (model.AORSubmission, error) {
|
||||
var aorSubmission model.AORSubmission
|
||||
if err := env.db.First(&aorSubmission, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return aorSubmission, datastore.ErrNotExist
|
||||
}
|
||||
return aorSubmission, err
|
||||
}
|
||||
return aorSubmission, nil
|
||||
}
|
||||
|
||||
func (env *AORSubmissions) GetByAOREvent(ctx context.Context, eventID int64) ([]model.AORSubmission, error) {
|
||||
var aorSubmissions []model.AORSubmission
|
||||
if err := env.db.Where("aor_event_id = ?", eventID).Order("added_at DESC").Find(&aorSubmissions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aorSubmissions, nil
|
||||
}
|
||||
|
||||
func (env *AORSubmissions) GetBySubmission(ctx context.Context, submissionID int64) ([]model.AORSubmission, error) {
|
||||
var aorSubmissions []model.AORSubmission
|
||||
if err := env.db.Where("submission_id = ?", submissionID).Order("added_at DESC").Find(&aorSubmissions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aorSubmissions, nil
|
||||
}
|
||||
|
||||
func (env *AORSubmissions) Create(ctx context.Context, aorSubmission model.AORSubmission) (model.AORSubmission, error) {
|
||||
if err := env.db.Create(&aorSubmission).Error; err != nil {
|
||||
return aorSubmission, err
|
||||
}
|
||||
return aorSubmission, nil
|
||||
}
|
||||
|
||||
func (env *AORSubmissions) Delete(ctx context.Context, id int64) error {
|
||||
if err := env.db.Delete(&model.AORSubmission{}, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return datastore.ErrNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (env *AORSubmissions) ListWithSubmissions(ctx context.Context, eventID int64) ([]model.Submission, error) {
|
||||
var submissions []model.Submission
|
||||
if err := env.db.
|
||||
Joins("JOIN aor_submissions ON aor_submissions.submission_id = submissions.id").
|
||||
Where("aor_submissions.aor_event_id = ?", eventID).
|
||||
Order("aor_submissions.added_at DESC").
|
||||
Find(&submissions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return submissions, nil
|
||||
}
|
||||
@@ -31,11 +31,14 @@ func New(ctx *cli.Context) (datastore.Datastore, error) {
|
||||
|
||||
if ctx.Bool("migrate") {
|
||||
if err := db.AutoMigrate(
|
||||
&model.AOREvent{},
|
||||
&model.AORSubmission{},
|
||||
&model.AuditEvent{},
|
||||
&model.Map{},
|
||||
&model.Mapfix{},
|
||||
&model.Operation{},
|
||||
&model.Submission{},
|
||||
&model.SubmissionReview{},
|
||||
&model.Script{},
|
||||
&model.ScriptPolicy{},
|
||||
); err != nil {
|
||||
|
||||
@@ -9,6 +9,14 @@ type Gormstore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (g Gormstore) AOREvents() datastore.AOREvents {
|
||||
return &AOREvents{db: g.db}
|
||||
}
|
||||
|
||||
func (g Gormstore) AORSubmissions() datastore.AORSubmissions {
|
||||
return &AORSubmissions{db: g.db}
|
||||
}
|
||||
|
||||
func (g Gormstore) AuditEvents() datastore.AuditEvents {
|
||||
return &AuditEvents{db: g.db}
|
||||
}
|
||||
@@ -29,6 +37,10 @@ func (g Gormstore) Submissions() datastore.Submissions {
|
||||
return &Submissions{db: g.db}
|
||||
}
|
||||
|
||||
func (g Gormstore) SubmissionReviews() datastore.SubmissionReviews {
|
||||
return &SubmissionReviews{db: g.db}
|
||||
}
|
||||
|
||||
func (g Gormstore) Scripts() datastore.Scripts {
|
||||
return &Scripts{db: g.db}
|
||||
}
|
||||
|
||||
83
pkg/datastore/gormstore/submission_reviews.go
Normal file
83
pkg/datastore/gormstore/submission_reviews.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package gormstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SubmissionReviews struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (env *SubmissionReviews) Get(ctx context.Context, id int64) (model.SubmissionReview, error) {
|
||||
var review model.SubmissionReview
|
||||
if err := env.db.First(&review, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return review, datastore.ErrNotExist
|
||||
}
|
||||
return review, err
|
||||
}
|
||||
return review, nil
|
||||
}
|
||||
|
||||
func (env *SubmissionReviews) GetBySubmissionAndReviewer(ctx context.Context, submissionID int64, reviewerID uint64) (model.SubmissionReview, error) {
|
||||
var review model.SubmissionReview
|
||||
if err := env.db.Where("submission_id = ? AND reviewer_id = ?", submissionID, reviewerID).First(&review).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return review, datastore.ErrNotExist
|
||||
}
|
||||
return review, err
|
||||
}
|
||||
return review, nil
|
||||
}
|
||||
|
||||
func (env *SubmissionReviews) Create(ctx context.Context, review model.SubmissionReview) (model.SubmissionReview, error) {
|
||||
if err := env.db.Create(&review).Error; err != nil {
|
||||
return review, err
|
||||
}
|
||||
|
||||
return review, nil
|
||||
}
|
||||
|
||||
func (env *SubmissionReviews) Update(ctx context.Context, id int64, values datastore.OptionalMap) error {
|
||||
if err := env.db.Model(&model.SubmissionReview{}).Where("id = ?", id).Updates(values.Map()).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return datastore.ErrNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (env *SubmissionReviews) Delete(ctx context.Context, id int64) error {
|
||||
if err := env.db.Delete(&model.SubmissionReview{}, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return datastore.ErrNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (env *SubmissionReviews) ListBySubmission(ctx context.Context, submissionID int64) ([]model.SubmissionReview, error) {
|
||||
var reviews []model.SubmissionReview
|
||||
if err := env.db.Where("submission_id = ?", submissionID).Order("created_at DESC").Find(&reviews).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reviews, nil
|
||||
}
|
||||
|
||||
func (env *SubmissionReviews) MarkOutdatedBySubmission(ctx context.Context, submissionID int64) error {
|
||||
if err := env.db.Model(&model.SubmissionReview{}).Where("submission_id = ?", submissionID).Update("outdated", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
37
pkg/model/aor_event.go
Normal file
37
pkg/model/aor_event.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type AOREventStatus int32
|
||||
|
||||
const (
|
||||
AOREventStatusScheduled AOREventStatus = 0 // Event scheduled, waiting for start
|
||||
AOREventStatusOpen AOREventStatus = 1 // Event started, accepting submissions (1st of month)
|
||||
AOREventStatusFrozen AOREventStatus = 2 // Submissions frozen (after 1st of month)
|
||||
AOREventStatusSelected AOREventStatus = 3 // Submissions selected for AOR (after week 1)
|
||||
AOREventStatusCompleted AOREventStatus = 4 // Decisions finalized (end of month)
|
||||
AOREventStatusClosed AOREventStatus = 5 // Event closed/archived
|
||||
)
|
||||
|
||||
// AOREvent represents an Accept or Reject event cycle
|
||||
// AOR events occur every 4 months (April, August, December)
|
||||
type AOREvent struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
StartDate time.Time `gorm:"index"` // 1st day of AOR month
|
||||
FreezeDate time.Time // End of 1st day (23:59:59)
|
||||
SelectionDate time.Time // End of week 1 (7 days after start)
|
||||
DecisionDate time.Time // End of month (when final decisions are made)
|
||||
Status AOREventStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// AORSubmission represents a submission that was added to an AOR event
|
||||
type AORSubmission struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
AOREventID int64 `gorm:"index"`
|
||||
SubmissionID int64 `gorm:"index"`
|
||||
AddedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -17,7 +17,7 @@ type ScriptPolicy struct {
|
||||
// Hash of the source code that leads to this policy.
|
||||
// If this is a replacement mapping, the original source may not be pointed to by any policy.
|
||||
// The original source should still exist in the scripts table, which can be located by the same hash.
|
||||
FromScriptHash int64 // postgres does not support unsigned integers, so we have to pretend
|
||||
FromScriptHash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
|
||||
// The ID of the replacement source (ScriptPolicyReplace)
|
||||
// or verbatim source (ScriptPolicyAllowed)
|
||||
// or 0 (other)
|
||||
|
||||
@@ -26,7 +26,7 @@ func HashParse(hash string) (uint64, error){
|
||||
type Script struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
Name string
|
||||
Hash int64 // postgres does not support unsigned integers, so we have to pretend
|
||||
Hash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
|
||||
Source string
|
||||
ResourceType ResourceType // is this a submission or is it a mapfix
|
||||
ResourceID int64 // which submission / mapfix did this script first appear in
|
||||
|
||||
14
pkg/model/submission_review.go
Normal file
14
pkg/model/submission_review.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type SubmissionReview struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
SubmissionID int64 `gorm:"index"`
|
||||
ReviewerID uint64
|
||||
Recommend bool
|
||||
Description string
|
||||
Outdated bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
160
pkg/roblox/thumbnails.go
Normal file
160
pkg/roblox/thumbnails.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package roblox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ThumbnailSize represents valid Roblox thumbnail sizes
|
||||
type ThumbnailSize string
|
||||
|
||||
const (
|
||||
Size150x150 ThumbnailSize = "150x150"
|
||||
Size420x420 ThumbnailSize = "420x420"
|
||||
Size768x432 ThumbnailSize = "768x432"
|
||||
)
|
||||
|
||||
// ThumbnailFormat represents the image format
|
||||
type ThumbnailFormat string
|
||||
|
||||
const (
|
||||
FormatPng ThumbnailFormat = "Png"
|
||||
FormatJpeg ThumbnailFormat = "Jpeg"
|
||||
)
|
||||
|
||||
// ThumbnailRequest represents a single thumbnail request
|
||||
type ThumbnailRequest struct {
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
Type string `json:"type"`
|
||||
TargetID uint64 `json:"targetId"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
}
|
||||
|
||||
// ThumbnailData represents a single thumbnail response
|
||||
type ThumbnailData struct {
|
||||
TargetID uint64 `json:"targetId"`
|
||||
State string `json:"state"` // "Completed", "Error", "Pending"
|
||||
ImageURL string `json:"imageUrl"`
|
||||
}
|
||||
|
||||
// BatchThumbnailsResponse represents the API response
|
||||
type BatchThumbnailsResponse struct {
|
||||
Data []ThumbnailData `json:"data"`
|
||||
}
|
||||
|
||||
// GetAssetThumbnails fetches thumbnails for multiple assets in a single batch request
|
||||
// Roblox allows up to 100 assets per batch
|
||||
func (c *Client) GetAssetThumbnails(assetIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
|
||||
if len(assetIDs) == 0 {
|
||||
return []ThumbnailData{}, nil
|
||||
}
|
||||
if len(assetIDs) > 100 {
|
||||
return nil, GetError("batch size cannot exceed 100 assets")
|
||||
}
|
||||
|
||||
// Build request payload - the API expects an array directly, not wrapped in an object
|
||||
requests := make([]ThumbnailRequest, len(assetIDs))
|
||||
for i, assetID := range assetIDs {
|
||||
requests[i] = ThumbnailRequest{
|
||||
Type: "Asset",
|
||||
TargetID: assetID,
|
||||
Size: string(size),
|
||||
Format: string(format),
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requests)
|
||||
if err != nil {
|
||||
return nil, GetError("JSONMarshalError: " + err.Error())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, GetError("RequestCreationError: " + err.Error())
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, GetError("RequestError: " + err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, GetError("ReadBodyError: " + err.Error())
|
||||
}
|
||||
|
||||
var response BatchThumbnailsResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, GetError("JSONUnmarshalError: " + err.Error())
|
||||
}
|
||||
|
||||
return response.Data, nil
|
||||
}
|
||||
|
||||
// GetUserAvatarThumbnails fetches avatar thumbnails for multiple users in a single batch request
|
||||
func (c *Client) GetUserAvatarThumbnails(userIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return []ThumbnailData{}, nil
|
||||
}
|
||||
if len(userIDs) > 100 {
|
||||
return nil, GetError("batch size cannot exceed 100 users")
|
||||
}
|
||||
|
||||
// Build request payload - the API expects an array directly, not wrapped in an object
|
||||
requests := make([]ThumbnailRequest, len(userIDs))
|
||||
for i, userID := range userIDs {
|
||||
requests[i] = ThumbnailRequest{
|
||||
Type: "AvatarHeadShot",
|
||||
TargetID: userID,
|
||||
Size: string(size),
|
||||
Format: string(format),
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requests)
|
||||
if err != nil {
|
||||
return nil, GetError("JSONMarshalError: " + err.Error())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, GetError("RequestCreationError: " + err.Error())
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, GetError("RequestError: " + err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, GetError("ReadBodyError: " + err.Error())
|
||||
}
|
||||
|
||||
var response BatchThumbnailsResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, GetError("JSONUnmarshalError: " + err.Error())
|
||||
}
|
||||
|
||||
return response.Data, nil
|
||||
}
|
||||
72
pkg/roblox/users.go
Normal file
72
pkg/roblox/users.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package roblox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserData represents a single user's information
|
||||
type UserData struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
// BatchUsersResponse represents the API response for batch user requests
|
||||
type BatchUsersResponse struct {
|
||||
Data []UserData `json:"data"`
|
||||
}
|
||||
|
||||
// GetUsernames fetches usernames for multiple users in a single batch request
|
||||
// Roblox allows up to 100 users per batch
|
||||
func (c *Client) GetUsernames(userIDs []uint64) ([]UserData, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return []UserData{}, nil
|
||||
}
|
||||
if len(userIDs) > 100 {
|
||||
return nil, GetError("batch size cannot exceed 100 users")
|
||||
}
|
||||
|
||||
// Build request payload
|
||||
payload := map[string][]uint64{
|
||||
"userIds": userIDs,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, GetError("JSONMarshalError: " + err.Error())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://users.roblox.com/v1/users", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, GetError("RequestCreationError: " + err.Error())
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, GetError("RequestError: " + err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, GetError("ReadBodyError: " + err.Error())
|
||||
}
|
||||
|
||||
var response BatchUsersResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, GetError("JSONUnmarshalError: " + err.Error())
|
||||
}
|
||||
|
||||
return response.Data, nil
|
||||
}
|
||||
30
pkg/service/aor_events.go
Normal file
30
pkg/service/aor_events.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
)
|
||||
|
||||
// AOR Event service methods
|
||||
|
||||
func (svc *Service) GetAOREvent(ctx context.Context, id int64) (model.AOREvent, error) {
|
||||
return svc.db.AOREvents().Get(ctx, id)
|
||||
}
|
||||
|
||||
func (svc *Service) GetActiveAOREvent(ctx context.Context) (model.AOREvent, error) {
|
||||
return svc.db.AOREvents().GetActive(ctx)
|
||||
}
|
||||
|
||||
func (svc *Service) ListAOREvents(ctx context.Context, page model.Page) ([]model.AOREvent, error) {
|
||||
return svc.db.AOREvents().List(ctx, datastore.Optional(), page)
|
||||
}
|
||||
|
||||
func (svc *Service) GetAORSubmissionsByEvent(ctx context.Context, eventID int64) ([]model.Submission, error) {
|
||||
return svc.db.AORSubmissions().ListWithSubmissions(ctx, eventID)
|
||||
}
|
||||
|
||||
func (svc *Service) GetAORSubmissionsBySubmission(ctx context.Context, submissionID int64) ([]model.AORSubmission, error) {
|
||||
return svc.db.AORSubmissions().GetBySubmission(ctx, submissionID)
|
||||
}
|
||||
389
pkg/service/aor_scheduler.go
Normal file
389
pkg/service/aor_scheduler.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// AORScheduler manages AOR events and their lifecycle
|
||||
type AORScheduler struct {
|
||||
ds datastore.Datastore
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewAORScheduler creates a new AOR scheduler
|
||||
func NewAORScheduler(ds datastore.Datastore) *AORScheduler {
|
||||
return &AORScheduler{
|
||||
ds: ds,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessAOREvents is the main entry point for the cron job
|
||||
// It checks and updates AOR event statuses
|
||||
func (s *AORScheduler) ProcessAOREvents() error {
|
||||
log.Info("AOR Scheduler: Processing events")
|
||||
|
||||
// Initialize: create next AOR event if none exists
|
||||
if err := s.ensureNextAOREvent(); err != nil {
|
||||
log.WithError(err).Error("Failed to ensure next AOR event")
|
||||
return err
|
||||
}
|
||||
|
||||
// Process current active event
|
||||
if err := s.processAOREvents(); err != nil {
|
||||
log.WithError(err).Error("Failed to process AOR events")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("AOR Scheduler: Processing completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureNextAOREvent creates the next AOR event if one doesn't exist
|
||||
func (s *AORScheduler) ensureNextAOREvent() error {
|
||||
// Check if there's an active or scheduled event
|
||||
_, err := s.ds.AOREvents().GetActive(s.ctx)
|
||||
if err == nil {
|
||||
// Event exists, nothing to do
|
||||
return nil
|
||||
}
|
||||
if err != datastore.ErrNotExist {
|
||||
return err
|
||||
}
|
||||
|
||||
// No active event, create the next one
|
||||
nextDate := s.calculateNextAORDate(time.Now())
|
||||
return s.createAOREvent(nextDate)
|
||||
}
|
||||
|
||||
// calculateNextAORDate calculates the next AOR start date
|
||||
// AOR events are held every 4 months: April, August, December
|
||||
func (s *AORScheduler) calculateNextAORDate(from time.Time) time.Time {
|
||||
aorMonths := []time.Month{time.April, time.August, time.December}
|
||||
|
||||
currentYear := from.Year()
|
||||
currentMonth := from.Month()
|
||||
|
||||
// Find the next AOR month
|
||||
for _, month := range aorMonths {
|
||||
if month > currentMonth {
|
||||
// Next AOR is this year
|
||||
return time.Date(currentYear, month, 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
// Next AOR is in April of next year
|
||||
return time.Date(currentYear+1, time.April, 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
// createAOREvent creates a new AOR event with calculated dates
|
||||
func (s *AORScheduler) createAOREvent(startDate time.Time) error {
|
||||
freezeDate := startDate.Add(24*time.Hour - time.Second) // End of first day (23:59:59)
|
||||
selectionDate := startDate.Add(7 * 24 * time.Hour) // 7 days after start
|
||||
|
||||
// Decision date is the last day of the month at 23:59:59
|
||||
// Calculate the first day of next month, then subtract 1 second
|
||||
year, month, _ := startDate.Date()
|
||||
firstOfNextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, time.UTC)
|
||||
decisionDate := firstOfNextMonth.Add(-time.Second)
|
||||
|
||||
event := model.AOREvent{
|
||||
StartDate: startDate,
|
||||
FreezeDate: freezeDate,
|
||||
SelectionDate: selectionDate,
|
||||
DecisionDate: decisionDate,
|
||||
Status: model.AOREventStatusScheduled,
|
||||
}
|
||||
|
||||
_, err := s.ds.AOREvents().Create(s.ctx, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"start_date": startDate,
|
||||
"freeze_date": freezeDate,
|
||||
"selection_date": selectionDate,
|
||||
"decision_date": decisionDate,
|
||||
}).Info("Created new AOR event")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processAOREvents checks and updates AOR event statuses
|
||||
func (s *AORScheduler) processAOREvents() error {
|
||||
now := time.Now()
|
||||
|
||||
// Get active event
|
||||
event, err := s.ds.AOREvents().GetActive(s.ctx)
|
||||
if err == datastore.ErrNotExist {
|
||||
// No active event, ensure one is created
|
||||
return s.ensureNextAOREvent()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process event based on current status and dates
|
||||
switch event.Status {
|
||||
case model.AOREventStatusScheduled:
|
||||
// Check if event should start (it's now the 1st of the AOR month)
|
||||
if now.After(event.StartDate) || now.Equal(event.StartDate) {
|
||||
if err := s.openAOREvent(event.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case model.AOREventStatusOpen:
|
||||
// Check if submissions should be frozen (past the freeze date)
|
||||
if now.After(event.FreezeDate) {
|
||||
if err := s.freezeAOREvent(event.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case model.AOREventStatusFrozen:
|
||||
// Check if it's time to select submissions (past selection date)
|
||||
if now.After(event.SelectionDate) {
|
||||
if err := s.selectSubmissions(event.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case model.AOREventStatusSelected:
|
||||
// Check if it's time to finalize decisions (past decision date)
|
||||
if now.After(event.DecisionDate) {
|
||||
if err := s.finalizeDecisions(event.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case model.AOREventStatusCompleted:
|
||||
// Event completed, create next one and close this one
|
||||
nextDate := s.calculateNextAORDate(event.StartDate)
|
||||
if err := s.createAOREvent(nextDate); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.closeAOREvent(event.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// openAOREvent transitions an event to Open status
|
||||
func (s *AORScheduler) openAOREvent(eventID int64) error {
|
||||
err := s.ds.AOREvents().Update(s.ctx, eventID, datastore.Optional().Add("status", model.AOREventStatusOpen))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithField("event_id", eventID).Info("AOR event opened - submissions now accepted")
|
||||
return nil
|
||||
}
|
||||
|
||||
// freezeAOREvent transitions an event to Frozen status
|
||||
// TODO: lock submission from updates
|
||||
func (s *AORScheduler) freezeAOREvent(eventID int64) error {
|
||||
err := s.ds.AOREvents().Update(s.ctx, eventID, datastore.Optional().Add("status", model.AOREventStatusFrozen))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithField("event_id", eventID).Info("AOR event frozen - submissions locked")
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectSubmissions automatically selects qualifying submissions
|
||||
func (s *AORScheduler) selectSubmissions(eventID int64) error {
|
||||
// Get all submissions in Submitted status
|
||||
submissions, err := s.ds.Submissions().List(s.ctx, datastore.Optional().Add("status_id", model.SubmissionStatusSubmitted), model.Page{Number: 0, Size: 0}, datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedCount := 0
|
||||
for _, submission := range submissions {
|
||||
// Get all reviews for this submission
|
||||
reviews, err := s.ds.SubmissionReviews().ListBySubmission(s.ctx, submission.ID)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("submission_id", submission.ID).Error("Failed to get reviews")
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply selection criteria
|
||||
if s.shouldAddToAOR(reviews) {
|
||||
// Add to AOR event
|
||||
aorSubmission := model.AORSubmission{
|
||||
AOREventID: eventID,
|
||||
SubmissionID: submission.ID,
|
||||
AddedAt: time.Now(),
|
||||
}
|
||||
_, err := s.ds.AORSubmissions().Create(s.ctx, aorSubmission)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("submission_id", submission.ID).Error("Failed to add submission to AOR")
|
||||
continue
|
||||
}
|
||||
selectedCount++
|
||||
log.WithField("submission_id", submission.ID).Info("Added submission to AOR event")
|
||||
}
|
||||
}
|
||||
|
||||
// Mark event as selected (waiting for end of month to finalize)
|
||||
err = s.ds.AOREvents().Update(s.ctx, eventID, datastore.Optional().Add("status", model.AOREventStatusSelected))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"event_id": eventID,
|
||||
"selected_count": selectedCount,
|
||||
}).Info("AOR submission selection completed - waiting for end of month to finalize decisions")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldAddToAOR determines if a submission should be added to the AOR event
|
||||
// Criteria:
|
||||
// - If there are 0 reviews: NOT added
|
||||
// - If there is 1+ review with recommend=true and not outdated: added
|
||||
// - If majority (>=50%) of non-outdated reviews recommend: added
|
||||
// TODO: Audit events
|
||||
func (s *AORScheduler) shouldAddToAOR(reviews []model.SubmissionReview) bool {
|
||||
// Filter out outdated reviews
|
||||
var validReviews []model.SubmissionReview
|
||||
for _, review := range reviews {
|
||||
if !review.Outdated {
|
||||
validReviews = append(validReviews, review)
|
||||
}
|
||||
}
|
||||
|
||||
// If there are 0 valid reviews, don't add
|
||||
if len(validReviews) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Count recommendations
|
||||
recommendCount := 0
|
||||
for _, review := range validReviews {
|
||||
if review.Recommend {
|
||||
recommendCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Need at least 50% recommendations (2 accept + 2 deny = 50% = added)
|
||||
// This means recommendCount * 2 >= len(validReviews)
|
||||
return recommendCount*2 >= len(validReviews)
|
||||
}
|
||||
|
||||
// shouldAccept determines if a submission should be accepted in final decisions
|
||||
// Criteria: Must have >50% (strictly greater than) recommendations
|
||||
func (s *AORScheduler) shouldAccept(reviews []model.SubmissionReview) bool {
|
||||
// Filter out outdated reviews
|
||||
var validReviews []model.SubmissionReview
|
||||
for _, review := range reviews {
|
||||
if !review.Outdated {
|
||||
validReviews = append(validReviews, review)
|
||||
}
|
||||
}
|
||||
|
||||
// If there are 0 valid reviews, don't accept
|
||||
if len(validReviews) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Count recommendations
|
||||
recommendCount := 0
|
||||
for _, review := range validReviews {
|
||||
if review.Recommend {
|
||||
recommendCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Need MORE than 50% recommendations (strictly greater)
|
||||
// This means recommendCount * 2 > len(validReviews)
|
||||
return recommendCount*2 > len(validReviews)
|
||||
}
|
||||
|
||||
// finalizeDecisions makes final accept/reject decisions at end of month
|
||||
// Submissions in the AOR event with >50% recommends are accepted
|
||||
// Submissions in the AOR event with <=50% recommends are rejected
|
||||
// TODO: Implement acceptance logic
|
||||
// TODO: Query roblox group to get get min votes needed for acceptance
|
||||
// TODO: Audit events
|
||||
func (s *AORScheduler) finalizeDecisions(eventID int64) error {
|
||||
// Get all submissions that were selected for this AOR event
|
||||
aorSubmissions, err := s.ds.AORSubmissions().GetByAOREvent(s.ctx, eventID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
acceptedCount := 0
|
||||
rejectedCount := 0
|
||||
|
||||
// Process each submission in the AOR event
|
||||
for _, aorSub := range aorSubmissions {
|
||||
// Get the submission
|
||||
submission, err := s.ds.Submissions().Get(s.ctx, aorSub.SubmissionID)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("submission_id", aorSub.SubmissionID).Error("Failed to get submission")
|
||||
continue
|
||||
}
|
||||
|
||||
// Get all reviews for this submission
|
||||
reviews, err := s.ds.SubmissionReviews().ListBySubmission(s.ctx, aorSub.SubmissionID)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("submission_id", aorSub.SubmissionID).Error("Failed to get reviews")
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if submission has >50% recommends (strictly greater)
|
||||
if s.shouldAccept(reviews) {
|
||||
// This submission has >50% recommends - accept it
|
||||
// TODO: Implement acceptance logic
|
||||
// For now, this is a placeholder
|
||||
log.WithField("submission_id", submission.ID).Info("TODO: Accept submission (placeholder)")
|
||||
acceptedCount++
|
||||
} else {
|
||||
// This submission does not have >50% recommends - reject it
|
||||
err := s.ds.Submissions().Update(s.ctx, submission.ID, datastore.Optional().Add("status_id", model.SubmissionStatusRejected))
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("submission_id", submission.ID).Error("Failed to reject submission")
|
||||
continue
|
||||
}
|
||||
log.WithField("submission_id", submission.ID).Info("Rejected submission")
|
||||
rejectedCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Mark event as completed
|
||||
err = s.ds.AOREvents().Update(s.ctx, eventID, datastore.Optional().Add("status", model.AOREventStatusCompleted))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"event_id": eventID,
|
||||
"accepted_count": acceptedCount,
|
||||
"rejected_count": rejectedCount,
|
||||
}).Info("AOR decisions finalized")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeAOREvent transitions an event to Closed status
|
||||
func (s *AORScheduler) closeAOREvent(eventID int64) error {
|
||||
err := s.ds.AOREvents().Update(s.ctx, eventID, datastore.Optional().Add("status", model.AOREventStatusClosed))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithField("event_id", eventID).Info("AOR event closed")
|
||||
return nil
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/go-grpc/maps"
|
||||
"git.itzana.me/strafesnet/go-grpc/users"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db datastore.Datastore
|
||||
nats nats.JetStreamContext
|
||||
maps maps.MapsServiceClient
|
||||
users users.UsersServiceClient
|
||||
db datastore.Datastore
|
||||
nats nats.JetStreamContext
|
||||
maps maps.MapsServiceClient
|
||||
users users.UsersServiceClient
|
||||
thumbnailService *ThumbnailService
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@@ -19,11 +24,44 @@ func NewService(
|
||||
nats nats.JetStreamContext,
|
||||
maps maps.MapsServiceClient,
|
||||
users users.UsersServiceClient,
|
||||
robloxClient *roblox.Client,
|
||||
redisClient *redis.Client,
|
||||
) Service {
|
||||
return Service{
|
||||
db: db,
|
||||
nats: nats,
|
||||
maps: maps,
|
||||
users: users,
|
||||
db: db,
|
||||
nats: nats,
|
||||
maps: maps,
|
||||
users: users,
|
||||
thumbnailService: NewThumbnailService(robloxClient, redisClient),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAssetThumbnails proxies to the thumbnail service
|
||||
func (s *Service) GetAssetThumbnails(ctx context.Context, assetIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||
return s.thumbnailService.GetAssetThumbnails(ctx, assetIDs, size)
|
||||
}
|
||||
|
||||
// GetUserAvatarThumbnails proxies to the thumbnail service
|
||||
func (s *Service) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||
return s.thumbnailService.GetUserAvatarThumbnails(ctx, userIDs, size)
|
||||
}
|
||||
|
||||
// GetSingleAssetThumbnail proxies to the thumbnail service
|
||||
func (s *Service) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||
return s.thumbnailService.GetSingleAssetThumbnail(ctx, assetID, size)
|
||||
}
|
||||
|
||||
// GetSingleUserAvatarThumbnail proxies to the thumbnail service
|
||||
func (s *Service) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||
return s.thumbnailService.GetSingleUserAvatarThumbnail(ctx, userID, size)
|
||||
}
|
||||
|
||||
// GetUsernames proxies to the thumbnail service
|
||||
func (s *Service) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
|
||||
return s.thumbnailService.GetUsernames(ctx, userIDs)
|
||||
}
|
||||
|
||||
// GetSingleUsername proxies to the thumbnail service
|
||||
func (s *Service) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
|
||||
return s.thumbnailService.GetSingleUsername(ctx, userID)
|
||||
}
|
||||
|
||||
55
pkg/service/submission_reviews.go
Normal file
55
pkg/service/submission_reviews.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
)
|
||||
|
||||
type SubmissionReviewUpdate datastore.OptionalMap
|
||||
|
||||
func NewSubmissionReviewUpdate() SubmissionReviewUpdate {
|
||||
update := datastore.Optional()
|
||||
return SubmissionReviewUpdate(update)
|
||||
}
|
||||
|
||||
func (update SubmissionReviewUpdate) SetRecommend(recommend bool) {
|
||||
datastore.OptionalMap(update).Add("recommend", recommend)
|
||||
}
|
||||
|
||||
func (update SubmissionReviewUpdate) SetDescription(description string) {
|
||||
datastore.OptionalMap(update).Add("description", description)
|
||||
}
|
||||
|
||||
func (update SubmissionReviewUpdate) SetOutdated(outdated bool) {
|
||||
datastore.OptionalMap(update).Add("outdated", outdated)
|
||||
}
|
||||
|
||||
func (svc *Service) CreateSubmissionReview(ctx context.Context, review model.SubmissionReview) (model.SubmissionReview, error) {
|
||||
return svc.db.SubmissionReviews().Create(ctx, review)
|
||||
}
|
||||
|
||||
func (svc *Service) GetSubmissionReview(ctx context.Context, id int64) (model.SubmissionReview, error) {
|
||||
return svc.db.SubmissionReviews().Get(ctx, id)
|
||||
}
|
||||
|
||||
func (svc *Service) GetSubmissionReviewBySubmissionAndReviewer(ctx context.Context, submissionID int64, reviewerID uint64) (model.SubmissionReview, error) {
|
||||
return svc.db.SubmissionReviews().GetBySubmissionAndReviewer(ctx, submissionID, reviewerID)
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateSubmissionReview(ctx context.Context, id int64, update SubmissionReviewUpdate) error {
|
||||
return svc.db.SubmissionReviews().Update(ctx, id, datastore.OptionalMap(update))
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteSubmissionReview(ctx context.Context, id int64) error {
|
||||
return svc.db.SubmissionReviews().Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (svc *Service) ListSubmissionReviewsBySubmission(ctx context.Context, submissionID int64) ([]model.SubmissionReview, error) {
|
||||
return svc.db.SubmissionReviews().ListBySubmission(ctx, submissionID)
|
||||
}
|
||||
|
||||
func (svc *Service) MarkSubmissionReviewsOutdated(ctx context.Context, submissionID int64) error {
|
||||
return svc.db.SubmissionReviews().MarkOutdatedBySubmission(ctx, submissionID)
|
||||
}
|
||||
218
pkg/service/thumbnails.go
Normal file
218
pkg/service/thumbnails.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type ThumbnailService struct {
|
||||
robloxClient *roblox.Client
|
||||
redisClient *redis.Client
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
func NewThumbnailService(robloxClient *roblox.Client, redisClient *redis.Client) *ThumbnailService {
|
||||
return &ThumbnailService{
|
||||
robloxClient: robloxClient,
|
||||
redisClient: redisClient,
|
||||
cacheTTL: 24 * time.Hour, // Cache thumbnails for 24 hours
|
||||
}
|
||||
}
|
||||
|
||||
// CachedThumbnail represents a cached thumbnail entry
|
||||
type CachedThumbnail struct {
|
||||
ImageURL string `json:"imageUrl"`
|
||||
State string `json:"state"`
|
||||
CachedAt time.Time `json:"cachedAt"`
|
||||
}
|
||||
|
||||
// GetAssetThumbnails fetches thumbnails with Redis caching and batching
|
||||
func (s *ThumbnailService) GetAssetThumbnails(ctx context.Context, assetIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||
if len(assetIDs) == 0 {
|
||||
return map[uint64]string{}, nil
|
||||
}
|
||||
|
||||
result := make(map[uint64]string)
|
||||
var missingIDs []uint64
|
||||
|
||||
// Try to get from cache first
|
||||
for _, assetID := range assetIDs {
|
||||
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", assetID, size)
|
||||
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
|
||||
|
||||
if err == redis.Nil {
|
||||
// Cache miss
|
||||
missingIDs = append(missingIDs, assetID)
|
||||
} else if err != nil {
|
||||
// Redis error - treat as cache miss
|
||||
missingIDs = append(missingIDs, assetID)
|
||||
} else {
|
||||
// Cache hit
|
||||
var thumbnail CachedThumbnail
|
||||
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
|
||||
result[assetID] = thumbnail.ImageURL
|
||||
} else {
|
||||
missingIDs = append(missingIDs, assetID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all were cached, return early
|
||||
if len(missingIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Batch fetch missing thumbnails from Roblox API
|
||||
// Split into batches of 100 (Roblox API limit)
|
||||
for i := 0; i < len(missingIDs); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(missingIDs) {
|
||||
end = len(missingIDs)
|
||||
}
|
||||
batch := missingIDs[i:end]
|
||||
|
||||
thumbnails, err := s.robloxClient.GetAssetThumbnails(batch, size, roblox.FormatPng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch thumbnails: %w", err)
|
||||
}
|
||||
|
||||
// Process results and cache them
|
||||
for _, thumb := range thumbnails {
|
||||
cached := CachedThumbnail{
|
||||
ImageURL: thumb.ImageURL,
|
||||
State: thumb.State,
|
||||
CachedAt: time.Now(),
|
||||
}
|
||||
|
||||
if thumb.State == "Completed" && thumb.ImageURL != "" {
|
||||
result[thumb.TargetID] = thumb.ImageURL
|
||||
}
|
||||
|
||||
// Cache the result (even if incomplete, to avoid repeated API calls)
|
||||
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", thumb.TargetID, size)
|
||||
cachedJSON, _ := json.Marshal(cached)
|
||||
|
||||
// Use shorter TTL for incomplete thumbnails
|
||||
ttl := s.cacheTTL
|
||||
if thumb.State != "Completed" {
|
||||
ttl = 5 * time.Minute
|
||||
}
|
||||
|
||||
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserAvatarThumbnails fetches user avatar thumbnails with Redis caching and batching
|
||||
func (s *ThumbnailService) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return map[uint64]string{}, nil
|
||||
}
|
||||
|
||||
result := make(map[uint64]string)
|
||||
var missingIDs []uint64
|
||||
|
||||
// Try to get from cache first
|
||||
for _, userID := range userIDs {
|
||||
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", userID, size)
|
||||
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
|
||||
|
||||
if err == redis.Nil {
|
||||
// Cache miss
|
||||
missingIDs = append(missingIDs, userID)
|
||||
} else if err != nil {
|
||||
// Redis error - treat as cache miss
|
||||
missingIDs = append(missingIDs, userID)
|
||||
} else {
|
||||
// Cache hit
|
||||
var thumbnail CachedThumbnail
|
||||
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
|
||||
result[userID] = thumbnail.ImageURL
|
||||
} else {
|
||||
missingIDs = append(missingIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all were cached, return early
|
||||
if len(missingIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Batch fetch missing thumbnails from Roblox API
|
||||
// Split into batches of 100 (Roblox API limit)
|
||||
for i := 0; i < len(missingIDs); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(missingIDs) {
|
||||
end = len(missingIDs)
|
||||
}
|
||||
batch := missingIDs[i:end]
|
||||
|
||||
thumbnails, err := s.robloxClient.GetUserAvatarThumbnails(batch, size, roblox.FormatPng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch user thumbnails: %w", err)
|
||||
}
|
||||
|
||||
// Process results and cache them
|
||||
for _, thumb := range thumbnails {
|
||||
cached := CachedThumbnail{
|
||||
ImageURL: thumb.ImageURL,
|
||||
State: thumb.State,
|
||||
CachedAt: time.Now(),
|
||||
}
|
||||
|
||||
if thumb.State == "Completed" && thumb.ImageURL != "" {
|
||||
result[thumb.TargetID] = thumb.ImageURL
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", thumb.TargetID, size)
|
||||
cachedJSON, _ := json.Marshal(cached)
|
||||
|
||||
// Use shorter TTL for incomplete thumbnails
|
||||
ttl := s.cacheTTL
|
||||
if thumb.State != "Completed" {
|
||||
ttl = 5 * time.Minute
|
||||
}
|
||||
|
||||
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetSingleAssetThumbnail is a convenience method for fetching a single asset thumbnail
|
||||
func (s *ThumbnailService) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||
results, err := s.GetAssetThumbnails(ctx, []uint64{assetID}, size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if url, ok := results[assetID]; ok {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("thumbnail not available for asset %d", assetID)
|
||||
}
|
||||
|
||||
// GetSingleUserAvatarThumbnail is a convenience method for fetching a single user avatar thumbnail
|
||||
func (s *ThumbnailService) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
|
||||
results, err := s.GetUserAvatarThumbnails(ctx, []uint64{userID}, size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if url, ok := results[userID]; ok {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("thumbnail not available for user %d", userID)
|
||||
}
|
||||
108
pkg/service/users.go
Normal file
108
pkg/service/users.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// CachedUser represents a cached user entry
|
||||
type CachedUser struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
CachedAt time.Time `json:"cachedAt"`
|
||||
}
|
||||
|
||||
// GetUsernames fetches usernames with Redis caching and batching
|
||||
func (s *ThumbnailService) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return map[uint64]string{}, nil
|
||||
}
|
||||
|
||||
result := make(map[uint64]string)
|
||||
var missingIDs []uint64
|
||||
|
||||
// Try to get from cache first
|
||||
for _, userID := range userIDs {
|
||||
cacheKey := fmt.Sprintf("user:name:%d", userID)
|
||||
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
|
||||
|
||||
if err == redis.Nil {
|
||||
// Cache miss
|
||||
missingIDs = append(missingIDs, userID)
|
||||
} else if err != nil {
|
||||
// Redis error - treat as cache miss
|
||||
missingIDs = append(missingIDs, userID)
|
||||
} else {
|
||||
// Cache hit
|
||||
var user CachedUser
|
||||
if err := json.Unmarshal([]byte(cached), &user); err == nil && user.Name != "" {
|
||||
result[userID] = user.Name
|
||||
} else {
|
||||
missingIDs = append(missingIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all were cached, return early
|
||||
if len(missingIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Batch fetch missing usernames from Roblox API
|
||||
// Split into batches of 100 (Roblox API limit)
|
||||
for i := 0; i < len(missingIDs); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(missingIDs) {
|
||||
end = len(missingIDs)
|
||||
}
|
||||
batch := missingIDs[i:end]
|
||||
|
||||
var users []roblox.UserData
|
||||
var err error
|
||||
users, err = s.robloxClient.GetUsernames(batch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch usernames: %w", err)
|
||||
}
|
||||
|
||||
// Process results and cache them
|
||||
for _, user := range users {
|
||||
cached := CachedUser{
|
||||
Name: user.Name,
|
||||
DisplayName: user.DisplayName,
|
||||
CachedAt: time.Now(),
|
||||
}
|
||||
|
||||
if user.Name != "" {
|
||||
result[user.ID] = user.Name
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
cacheKey := fmt.Sprintf("user:name:%d", user.ID)
|
||||
cachedJSON, _ := json.Marshal(cached)
|
||||
|
||||
// Cache usernames for a long time (7 days) since they rarely change
|
||||
s.redisClient.Set(ctx, cacheKey, cachedJSON, 7*24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetSingleUsername is a convenience method for fetching a single username
|
||||
func (s *ThumbnailService) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
|
||||
results, err := s.GetUsernames(ctx, []uint64{userID})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if name, ok := results[userID]; ok {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("username not available for user %d", userID)
|
||||
}
|
||||
121
pkg/web_api/aor_events.go
Normal file
121
pkg/web_api/aor_events.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
)
|
||||
|
||||
// ListAOREvents implements listAOREvents operation.
|
||||
//
|
||||
// Get list of AOR events.
|
||||
//
|
||||
// GET /aor-events
|
||||
func (svc *Service) ListAOREvents(ctx context.Context, params api.ListAOREventsParams) ([]api.AOREvent, error) {
|
||||
page := model.Page{
|
||||
Number: params.Page,
|
||||
Size: params.Limit,
|
||||
}
|
||||
|
||||
events, err := svc.inner.ListAOREvents(ctx, page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp []api.AOREvent
|
||||
for _, event := range events {
|
||||
resp = append(resp, api.AOREvent{
|
||||
ID: event.ID,
|
||||
StartDate: event.StartDate.Unix(),
|
||||
FreezeDate: event.FreezeDate.Unix(),
|
||||
SelectionDate: event.SelectionDate.Unix(),
|
||||
DecisionDate: event.DecisionDate.Unix(),
|
||||
Status: int32(event.Status),
|
||||
CreatedAt: event.CreatedAt.Unix(),
|
||||
UpdatedAt: event.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetActiveAOREvent implements getActiveAOREvent operation.
|
||||
//
|
||||
// Get the currently active AOR event.
|
||||
//
|
||||
// GET /aor-events/active
|
||||
func (svc *Service) GetActiveAOREvent(ctx context.Context) (*api.AOREvent, error) {
|
||||
event, err := svc.inner.GetActiveAOREvent(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.AOREvent{
|
||||
ID: event.ID,
|
||||
StartDate: event.StartDate.Unix(),
|
||||
FreezeDate: event.FreezeDate.Unix(),
|
||||
SelectionDate: event.SelectionDate.Unix(),
|
||||
DecisionDate: event.DecisionDate.Unix(),
|
||||
Status: int32(event.Status),
|
||||
CreatedAt: event.CreatedAt.Unix(),
|
||||
UpdatedAt: event.UpdatedAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAOREvent implements getAOREvent operation.
|
||||
//
|
||||
// Get a specific AOR event.
|
||||
//
|
||||
// GET /aor-events/{AOREventID}
|
||||
func (svc *Service) GetAOREvent(ctx context.Context, params api.GetAOREventParams) (*api.AOREvent, error) {
|
||||
event, err := svc.inner.GetAOREvent(ctx, params.AOREventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.AOREvent{
|
||||
ID: event.ID,
|
||||
StartDate: event.StartDate.Unix(),
|
||||
FreezeDate: event.FreezeDate.Unix(),
|
||||
SelectionDate: event.SelectionDate.Unix(),
|
||||
DecisionDate: event.DecisionDate.Unix(),
|
||||
Status: int32(event.Status),
|
||||
CreatedAt: event.CreatedAt.Unix(),
|
||||
UpdatedAt: event.UpdatedAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAOREventSubmissions implements getAOREventSubmissions operation.
|
||||
//
|
||||
// Get all submissions for a specific AOR event.
|
||||
//
|
||||
// GET /aor-events/{AOREventID}/submissions
|
||||
func (svc *Service) GetAOREventSubmissions(ctx context.Context, params api.GetAOREventSubmissionsParams) ([]api.Submission, error) {
|
||||
submissions, err := svc.inner.GetAORSubmissionsByEvent(ctx, params.AOREventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp []api.Submission
|
||||
for _, submission := range submissions {
|
||||
resp = append(resp, api.Submission{
|
||||
ID: submission.ID,
|
||||
DisplayName: submission.DisplayName,
|
||||
Creator: submission.Creator,
|
||||
GameID: int32(submission.GameID),
|
||||
CreatedAt: submission.CreatedAt.Unix(),
|
||||
UpdatedAt: submission.UpdatedAt.Unix(),
|
||||
Submitter: int64(submission.Submitter),
|
||||
AssetID: int64(submission.AssetID),
|
||||
AssetVersion: int64(submission.AssetVersion),
|
||||
ValidatedAssetID: api.NewOptInt64(int64(submission.ValidatedAssetID)),
|
||||
ValidatedAssetVersion: api.NewOptInt64(int64(submission.ValidatedAssetVersion)),
|
||||
Completed: submission.Completed,
|
||||
UploadedAssetID: api.NewOptInt64(int64(submission.UploadedAssetID)),
|
||||
StatusID: int32(submission.StatusID),
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -327,6 +327,48 @@ func (svc *Service) UpdateMapfixModel(ctx context.Context, params api.UpdateMapf
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateMapfixDescription implements updateMapfixDescription operation.
|
||||
//
|
||||
// Update description (submitter only, status ChangesRequested or UnderConstruction).
|
||||
//
|
||||
// PATCH /mapfixes/{MapfixID}/description
|
||||
func (svc *Service) UpdateMapfixDescription(ctx context.Context, req api.UpdateMapfixDescriptionReq, params api.UpdateMapfixDescriptionParams) error {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return ErrUserInfo
|
||||
}
|
||||
|
||||
// read mapfix
|
||||
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if caller is the submitter
|
||||
if userId != mapfix.Submitter {
|
||||
return ErrPermissionDeniedNotSubmitter
|
||||
}
|
||||
|
||||
// read the new description from request body
|
||||
data, err := io.ReadAll(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newDescription := string(data)
|
||||
|
||||
// check if Status is ChangesRequested or UnderConstruction
|
||||
update := service.NewMapfixUpdate()
|
||||
update.SetDescription(newDescription)
|
||||
allow_statuses := []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusUnderConstruction}
|
||||
return svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
|
||||
}
|
||||
|
||||
// ActionMapfixReject invokes actionMapfixReject operation.
|
||||
//
|
||||
// Role Reviewer changes status from Submitted -> Rejected.
|
||||
|
||||
@@ -36,10 +36,28 @@ func (svc *Service) CreateScript(ctx context.Context, req *api.ScriptCreate) (*a
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash := int64(model.HashSource(req.Source))
|
||||
|
||||
// Check if a script with this hash already exists
|
||||
filter := service.NewScriptFilter()
|
||||
filter.SetHash(hash)
|
||||
existingScripts, err := svc.inner.ListScripts(ctx, filter, model.Page{Number: 1, Size: 1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If script with this hash exists, return existing script ID
|
||||
if len(existingScripts) > 0 {
|
||||
return &api.ScriptID{
|
||||
ScriptID: existingScripts[0].ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create new script
|
||||
script, err := svc.inner.CreateScript(ctx, model.Script{
|
||||
ID: 0,
|
||||
Name: req.Name,
|
||||
Hash: int64(model.HashSource(req.Source)),
|
||||
Hash: hash,
|
||||
Source: req.Source,
|
||||
ResourceType: model.ResourceType(req.ResourceType),
|
||||
ResourceID: req.ResourceID.Or(0),
|
||||
|
||||
105
pkg/web_api/stats.go
Normal file
105
pkg/web_api/stats.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
)
|
||||
|
||||
// GET /stats
|
||||
func (svc *Service) GetStats(ctx context.Context) (*api.Stats, error) {
|
||||
// Get total submissions count
|
||||
totalSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, service.NewSubmissionFilter(), model.Page{
|
||||
Number: 1,
|
||||
Size: 0, // We only want the count, not the items
|
||||
}, datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get total mapfixes count
|
||||
totalMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, service.NewMapfixFilter(), model.Page{
|
||||
Number: 1,
|
||||
Size: 0, // We only want the count, not the items
|
||||
}, datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get released submissions count
|
||||
releasedSubmissionsFilter := service.NewSubmissionFilter()
|
||||
releasedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatusReleased})
|
||||
releasedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, releasedSubmissionsFilter, model.Page{
|
||||
Number: 1,
|
||||
Size: 0,
|
||||
}, datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get released mapfixes count
|
||||
releasedMapfixesFilter := service.NewMapfixFilter()
|
||||
releasedMapfixesFilter.SetStatuses([]model.MapfixStatus{model.MapfixStatusReleased})
|
||||
releasedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, releasedMapfixesFilter, model.Page{
|
||||
Number: 1,
|
||||
Size: 0,
|
||||
}, datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get submitted submissions count (under review)
|
||||
submittedSubmissionsFilter := service.NewSubmissionFilter()
|
||||
submittedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{
|
||||
model.SubmissionStatusUnderConstruction,
|
||||
model.SubmissionStatusChangesRequested,
|
||||
model.SubmissionStatusSubmitting,
|
||||
model.SubmissionStatusSubmitted,
|
||||
model.SubmissionStatusAcceptedUnvalidated,
|
||||
model.SubmissionStatusValidating,
|
||||
model.SubmissionStatusValidated,
|
||||
model.SubmissionStatusUploading,
|
||||
model.SubmissionStatusUploaded,
|
||||
})
|
||||
submittedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, submittedSubmissionsFilter, model.Page{
|
||||
Number: 1,
|
||||
Size: 0,
|
||||
}, datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get submitted mapfixes count (under review)
|
||||
submittedMapfixesFilter := service.NewMapfixFilter()
|
||||
submittedMapfixesFilter.SetStatuses([]model.MapfixStatus{
|
||||
model.MapfixStatusUnderConstruction,
|
||||
model.MapfixStatusChangesRequested,
|
||||
model.MapfixStatusSubmitting,
|
||||
model.MapfixStatusSubmitted,
|
||||
model.MapfixStatusAcceptedUnvalidated,
|
||||
model.MapfixStatusValidating,
|
||||
model.MapfixStatusValidated,
|
||||
model.MapfixStatusUploading,
|
||||
model.MapfixStatusUploaded,
|
||||
model.MapfixStatusReleasing,
|
||||
})
|
||||
submittedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, submittedMapfixesFilter, model.Page{
|
||||
Number: 1,
|
||||
Size: 0,
|
||||
}, datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.Stats{
|
||||
TotalSubmissions: totalSubmissions,
|
||||
TotalMapfixes: totalMapfixes,
|
||||
ReleasedSubmissions: releasedSubmissions,
|
||||
ReleasedMapfixes: releasedMapfixes,
|
||||
SubmittedSubmissions: submittedSubmissions,
|
||||
SubmittedMapfixes: submittedMapfixes,
|
||||
}, nil
|
||||
}
|
||||
207
pkg/web_api/submission_reviews.go
Normal file
207
pkg/web_api/submission_reviews.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrReviewNotOwner = errors.New("You can only edit your own review")
|
||||
ErrReviewNotSubmitted = errors.New("Reviews can only be created or edited when the submission is in Submitted status")
|
||||
)
|
||||
|
||||
// ListSubmissionReviews implements listSubmissionReviews operation.
|
||||
//
|
||||
// Get all reviews for a submission.
|
||||
//
|
||||
// GET /submissions/{SubmissionID}/reviews
|
||||
func (svc *Service) ListSubmissionReviews(ctx context.Context, params api.ListSubmissionReviewsParams) ([]api.SubmissionReview, error) {
|
||||
reviews, err := svc.inner.ListSubmissionReviewsBySubmission(ctx, params.SubmissionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp []api.SubmissionReview
|
||||
for _, review := range reviews {
|
||||
resp = append(resp, api.SubmissionReview{
|
||||
ID: review.ID,
|
||||
SubmissionID: review.SubmissionID,
|
||||
ReviewerID: int64(review.ReviewerID),
|
||||
Recommend: review.Recommend,
|
||||
Description: review.Description,
|
||||
Outdated: review.Outdated,
|
||||
CreatedAt: review.CreatedAt.Unix(),
|
||||
UpdatedAt: review.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CreateSubmissionReview implements createSubmissionReview operation.
|
||||
//
|
||||
// Create a review for a submission.
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/reviews
|
||||
func (svc *Service) CreateSubmissionReview(ctx context.Context, req *api.SubmissionReviewCreate, params api.CreateSubmissionReviewParams) (*api.SubmissionReview, error) {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return nil, ErrUserInfo
|
||||
}
|
||||
|
||||
// Check if caller has required role
|
||||
has_role, err := userInfo.HasRoleSubmissionReview()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has_role {
|
||||
return nil, ErrPermissionDeniedNeedRoleSubmissionReview
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if submission exists and is in Submitted status
|
||||
submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if submission.StatusID != model.SubmissionStatusSubmitted {
|
||||
return nil, ErrReviewNotSubmitted
|
||||
}
|
||||
|
||||
// Check if user already has a review for this submission
|
||||
existingReview, err := svc.inner.GetSubmissionReviewBySubmissionAndReviewer(ctx, params.SubmissionID, userId)
|
||||
if err != nil && !errors.Is(err, datastore.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If review exists, update it instead
|
||||
if err == nil {
|
||||
update := service.NewSubmissionReviewUpdate()
|
||||
update.SetRecommend(req.Recommend)
|
||||
update.SetDescription(req.Description)
|
||||
update.SetOutdated(false)
|
||||
|
||||
err = svc.inner.UpdateSubmissionReview(ctx, existingReview.ID, update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch updated review
|
||||
updatedReview, err := svc.inner.GetSubmissionReview(ctx, existingReview.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.SubmissionReview{
|
||||
ID: updatedReview.ID,
|
||||
SubmissionID: updatedReview.SubmissionID,
|
||||
ReviewerID: int64(updatedReview.ReviewerID),
|
||||
Recommend: updatedReview.Recommend,
|
||||
Description: updatedReview.Description,
|
||||
Outdated: updatedReview.Outdated,
|
||||
CreatedAt: updatedReview.CreatedAt.Unix(),
|
||||
UpdatedAt: updatedReview.UpdatedAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create new review
|
||||
review := model.SubmissionReview{
|
||||
SubmissionID: params.SubmissionID,
|
||||
ReviewerID: userId,
|
||||
Recommend: req.Recommend,
|
||||
Description: req.Description,
|
||||
Outdated: false,
|
||||
}
|
||||
|
||||
createdReview, err := svc.inner.CreateSubmissionReview(ctx, review)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.SubmissionReview{
|
||||
ID: createdReview.ID,
|
||||
SubmissionID: createdReview.SubmissionID,
|
||||
ReviewerID: int64(createdReview.ReviewerID),
|
||||
Recommend: createdReview.Recommend,
|
||||
Description: createdReview.Description,
|
||||
Outdated: createdReview.Outdated,
|
||||
CreatedAt: createdReview.CreatedAt.Unix(),
|
||||
UpdatedAt: createdReview.UpdatedAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateSubmissionReview implements updateSubmissionReview operation.
|
||||
//
|
||||
// Update an existing review.
|
||||
//
|
||||
// PATCH /submissions/{SubmissionID}/reviews/{ReviewID}
|
||||
func (svc *Service) UpdateSubmissionReview(ctx context.Context, req *api.SubmissionReviewCreate, params api.UpdateSubmissionReviewParams) (*api.SubmissionReview, error) {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return nil, ErrUserInfo
|
||||
}
|
||||
|
||||
userId, err := userInfo.GetUserID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the existing review
|
||||
review, err := svc.inner.GetSubmissionReview(ctx, params.ReviewID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if user is the owner of the review
|
||||
if review.ReviewerID != userId {
|
||||
return nil, ErrReviewNotOwner
|
||||
}
|
||||
|
||||
// Check if submission is still in Submitted status
|
||||
submission, err := svc.inner.GetSubmission(ctx, params.SubmissionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if submission.StatusID != model.SubmissionStatusSubmitted {
|
||||
return nil, ErrReviewNotSubmitted
|
||||
}
|
||||
|
||||
// Update the review
|
||||
update := service.NewSubmissionReviewUpdate()
|
||||
update.SetRecommend(req.Recommend)
|
||||
update.SetDescription(req.Description)
|
||||
update.SetOutdated(false) // Clear outdated flag on edit
|
||||
|
||||
err = svc.inner.UpdateSubmissionReview(ctx, params.ReviewID, update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch updated review
|
||||
updatedReview, err := svc.inner.GetSubmissionReview(ctx, params.ReviewID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.SubmissionReview{
|
||||
ID: updatedReview.ID,
|
||||
SubmissionID: updatedReview.SubmissionID,
|
||||
ReviewerID: int64(updatedReview.ReviewerID),
|
||||
Recommend: updatedReview.Recommend,
|
||||
Description: updatedReview.Description,
|
||||
Outdated: updatedReview.Outdated,
|
||||
CreatedAt: updatedReview.CreatedAt.Unix(),
|
||||
UpdatedAt: updatedReview.UpdatedAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
135
pkg/web_api/thumbnails.go
Normal file
135
pkg/web_api/thumbnails.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
|
||||
)
|
||||
|
||||
// BatchAssetThumbnails handles batch fetching of asset thumbnails
|
||||
func (svc *Service) BatchAssetThumbnails(ctx context.Context, req *api.BatchAssetThumbnailsReq) (*api.BatchAssetThumbnailsOK, error) {
|
||||
if len(req.AssetIds) == 0 {
|
||||
return &api.BatchAssetThumbnailsOK{
|
||||
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(map[string]string{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert size string to enum
|
||||
size := roblox.Size420x420
|
||||
if req.Size.IsSet() {
|
||||
sizeStr := req.Size.Value
|
||||
switch api.BatchAssetThumbnailsReqSize(sizeStr) {
|
||||
case api.BatchAssetThumbnailsReqSize150x150:
|
||||
size = roblox.Size150x150
|
||||
case api.BatchAssetThumbnailsReqSize768x432:
|
||||
size = roblox.Size768x432
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch thumbnails from service
|
||||
thumbnails, err := svc.inner.GetAssetThumbnails(ctx, req.AssetIds, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert map[uint64]string to map[string]string for JSON
|
||||
result := make(map[string]string, len(thumbnails))
|
||||
for assetID, url := range thumbnails {
|
||||
result[strconv.FormatUint(assetID, 10)] = url
|
||||
}
|
||||
|
||||
return &api.BatchAssetThumbnailsOK{
|
||||
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAssetThumbnail handles single asset thumbnail fetch (with redirect)
|
||||
func (svc *Service) GetAssetThumbnail(ctx context.Context, params api.GetAssetThumbnailParams) (*api.GetAssetThumbnailFound, error) {
|
||||
// Convert size string to enum
|
||||
size := roblox.Size420x420
|
||||
if params.Size.IsSet() {
|
||||
sizeStr := params.Size.Value
|
||||
switch api.GetAssetThumbnailSize(sizeStr) {
|
||||
case api.GetAssetThumbnailSize150x150:
|
||||
size = roblox.Size150x150
|
||||
case api.GetAssetThumbnailSize768x432:
|
||||
size = roblox.Size768x432
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch thumbnail
|
||||
thumbnailURL, err := svc.inner.GetSingleAssetThumbnail(ctx, params.AssetID, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return redirect response
|
||||
return &api.GetAssetThumbnailFound{
|
||||
Location: api.NewOptString(thumbnailURL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BatchUserThumbnails handles batch fetching of user avatar thumbnails
|
||||
func (svc *Service) BatchUserThumbnails(ctx context.Context, req *api.BatchUserThumbnailsReq) (*api.BatchUserThumbnailsOK, error) {
|
||||
if len(req.UserIds) == 0 {
|
||||
return &api.BatchUserThumbnailsOK{
|
||||
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(map[string]string{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert size string to enum
|
||||
size := roblox.Size150x150
|
||||
if req.Size.IsSet() {
|
||||
sizeStr := req.Size.Value
|
||||
switch api.BatchUserThumbnailsReqSize(sizeStr) {
|
||||
case api.BatchUserThumbnailsReqSize420x420:
|
||||
size = roblox.Size420x420
|
||||
case api.BatchUserThumbnailsReqSize768x432:
|
||||
size = roblox.Size768x432
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch thumbnails from service
|
||||
thumbnails, err := svc.inner.GetUserAvatarThumbnails(ctx, req.UserIds, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert map[uint64]string to map[string]string for JSON
|
||||
result := make(map[string]string, len(thumbnails))
|
||||
for userID, url := range thumbnails {
|
||||
result[strconv.FormatUint(userID, 10)] = url
|
||||
}
|
||||
|
||||
return &api.BatchUserThumbnailsOK{
|
||||
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserThumbnail handles single user avatar thumbnail fetch (with redirect)
|
||||
func (svc *Service) GetUserThumbnail(ctx context.Context, params api.GetUserThumbnailParams) (*api.GetUserThumbnailFound, error) {
|
||||
// Convert size string to enum
|
||||
size := roblox.Size150x150
|
||||
if params.Size.IsSet() {
|
||||
sizeStr := params.Size.Value
|
||||
switch api.GetUserThumbnailSize(sizeStr) {
|
||||
case api.GetUserThumbnailSize420x420:
|
||||
size = roblox.Size420x420
|
||||
case api.GetUserThumbnailSize768x432:
|
||||
size = roblox.Size768x432
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch thumbnail
|
||||
thumbnailURL, err := svc.inner.GetSingleUserAvatarThumbnail(ctx, params.UserID, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return redirect response
|
||||
return &api.GetUserThumbnailFound{
|
||||
Location: api.NewOptString(thumbnailURL),
|
||||
}, nil
|
||||
}
|
||||
33
pkg/web_api/users.go
Normal file
33
pkg/web_api/users.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||
)
|
||||
|
||||
// BatchUsernames handles batch fetching of usernames
|
||||
func (svc *Service) BatchUsernames(ctx context.Context, req *api.BatchUsernamesReq) (*api.BatchUsernamesOK, error) {
|
||||
if len(req.UserIds) == 0 {
|
||||
return &api.BatchUsernamesOK{
|
||||
Usernames: api.NewOptBatchUsernamesOKUsernames(map[string]string{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Fetch usernames from service
|
||||
usernames, err := svc.inner.GetUsernames(ctx, req.UserIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert map[uint64]string to map[string]string for JSON
|
||||
result := make(map[string]string, len(usernames))
|
||||
for userID, username := range usernames {
|
||||
result[strconv.FormatUint(userID, 10)] = username
|
||||
}
|
||||
|
||||
return &api.BatchUsernamesOK{
|
||||
Usernames: api.NewOptBatchUsernamesOKUsernames(result),
|
||||
}, nil
|
||||
}
|
||||
@@ -30,7 +30,6 @@ impl<Items> std::error::Error for SingleItemError<Items> where Items:std::fmt::D
|
||||
pub type ScriptSingleItemError=SingleItemError<Vec<ScriptID>>;
|
||||
pub type ScriptPolicySingleItemError=SingleItemError<Vec<ScriptPolicyID>>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct UrlAndBody{
|
||||
pub url:url::Url,
|
||||
@@ -76,7 +75,7 @@ pub enum GameID{
|
||||
FlyTrials=5,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateMapfixRequest<'a>{
|
||||
pub OperationID:OperationID,
|
||||
@@ -89,13 +88,13 @@ pub struct CreateMapfixRequest<'a>{
|
||||
pub TargetAssetID:u64,
|
||||
pub Description:&'a str,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct MapfixIDResponse{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateSubmissionRequest<'a>{
|
||||
pub OperationID:OperationID,
|
||||
@@ -108,7 +107,7 @@ pub struct CreateSubmissionRequest<'a>{
|
||||
pub Status:u32,
|
||||
pub Roles:u32,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct SubmissionIDResponse{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -127,11 +126,11 @@ pub enum ResourceType{
|
||||
Submission=2,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct GetScriptRequest{
|
||||
pub ScriptID:ScriptID,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct GetScriptsRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -151,7 +150,7 @@ pub struct GetScriptsRequest<'a>{
|
||||
pub struct HashRequest<'a>{
|
||||
pub hash:&'a str,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptResponse{
|
||||
pub ID:ScriptID,
|
||||
@@ -161,7 +160,7 @@ pub struct ScriptResponse{
|
||||
pub ResourceType:ResourceType,
|
||||
pub ResourceID:ResourceID,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateScriptRequest<'a>{
|
||||
pub Name:&'a str,
|
||||
@@ -170,7 +169,7 @@ pub struct CreateScriptRequest<'a>{
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub ResourceID:Option<ResourceID>,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptIDResponse{
|
||||
pub ScriptID:ScriptID,
|
||||
@@ -186,11 +185,11 @@ pub enum Policy{
|
||||
Replace=4,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct GetScriptPolicyRequest{
|
||||
pub ScriptPolicyID:ScriptPolicyID,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct GetScriptPoliciesRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -202,7 +201,7 @@ pub struct GetScriptPoliciesRequest<'a>{
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub Policy:Option<Policy>,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptPolicyResponse{
|
||||
pub ID:ScriptPolicyID,
|
||||
@@ -210,20 +209,20 @@ pub struct ScriptPolicyResponse{
|
||||
pub ToScriptID:ScriptID,
|
||||
pub Policy:Policy
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct CreateScriptPolicyRequest{
|
||||
pub FromScriptID:ScriptID,
|
||||
pub ToScriptID:ScriptID,
|
||||
pub Policy:Policy,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct ScriptPolicyIDResponse{
|
||||
pub ScriptPolicyID:ScriptPolicyID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct UpdateScriptPolicyRequest{
|
||||
pub ID:ScriptPolicyID,
|
||||
@@ -235,7 +234,7 @@ pub struct UpdateScriptPolicyRequest{
|
||||
pub Policy:Option<Policy>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct UpdateSubmissionModelRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -276,7 +275,7 @@ pub enum MapfixStatus{
|
||||
Released=10,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetMapfixesRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -292,7 +291,7 @@ pub struct GetMapfixesRequest<'a>{
|
||||
pub StatusID:Option<MapfixStatus>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
|
||||
pub struct MapfixResponse{
|
||||
pub ID:MapfixID,
|
||||
@@ -312,7 +311,7 @@ pub struct MapfixResponse{
|
||||
pub Description:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct MapfixesResponse{
|
||||
pub Total:u64,
|
||||
@@ -342,7 +341,7 @@ pub enum SubmissionStatus{
|
||||
Released=10,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetSubmissionsRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -358,7 +357,7 @@ pub struct GetSubmissionsRequest<'a>{
|
||||
pub StatusID:Option<SubmissionStatus>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct SubmissionResponse{
|
||||
pub ID:SubmissionID,
|
||||
@@ -376,14 +375,14 @@ pub struct SubmissionResponse{
|
||||
pub StatusID:SubmissionStatus,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct SubmissionsResponse{
|
||||
pub Total:u64,
|
||||
pub Submissions:Vec<SubmissionResponse>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetMapsRequest<'a>{
|
||||
pub Page:u32,
|
||||
@@ -394,7 +393,7 @@ pub struct GetMapsRequest<'a>{
|
||||
pub GameID:Option<GameID>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct MapResponse{
|
||||
pub ID:i64,
|
||||
@@ -404,7 +403,7 @@ pub struct MapResponse{
|
||||
pub Date:i64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetMapfixAuditEventsRequest{
|
||||
pub Page:u32,
|
||||
@@ -412,7 +411,7 @@ pub struct GetMapfixAuditEventsRequest{
|
||||
pub MapfixID:i64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct GetSubmissionAuditEventsRequest{
|
||||
pub Page:u32,
|
||||
@@ -420,7 +419,6 @@ pub struct GetSubmissionAuditEventsRequest{
|
||||
pub SubmissionID:i64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde_repr::Deserialize_repr)]
|
||||
#[repr(u32)]
|
||||
pub enum AuditEventType{
|
||||
@@ -475,7 +473,6 @@ pub struct AuditEventCheckList{
|
||||
pub check_list:Vec<AuditEventCheck>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub enum AuditEventData{
|
||||
Action(AuditEventAction),
|
||||
@@ -491,7 +488,7 @@ pub enum AuditEventData{
|
||||
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
|
||||
pub struct AuditEventID(pub(crate)i64);
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct AuditEventReponse{
|
||||
pub ID:AuditEventID,
|
||||
@@ -518,7 +515,7 @@ impl AuditEventReponse{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct Check{
|
||||
pub Name:&'static str,
|
||||
@@ -526,7 +523,7 @@ pub struct Check{
|
||||
pub Passed:bool,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionSubmittedRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -536,33 +533,33 @@ pub struct ActionSubmissionSubmittedRequest{
|
||||
pub GameID:GameID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionRequestChangesRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionUploadedRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
pub UploadedAssetID:u64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionSubmissionAcceptedRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateSubmissionAuditErrorRequest{
|
||||
pub SubmissionID:SubmissionID,
|
||||
pub ErrorMessage:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateSubmissionAuditCheckListRequest<'a>{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -580,7 +577,7 @@ impl SubmissionID{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct UpdateMapfixModelRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
@@ -588,7 +585,7 @@ pub struct UpdateMapfixModelRequest{
|
||||
pub ModelVersion:u64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixSubmittedRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
@@ -598,32 +595,32 @@ pub struct ActionMapfixSubmittedRequest{
|
||||
pub GameID:GameID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixRequestChangesRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixUploadedRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionMapfixAcceptedRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateMapfixAuditErrorRequest{
|
||||
pub MapfixID:MapfixID,
|
||||
pub ErrorMessage:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct CreateMapfixAuditCheckListRequest<'a>{
|
||||
pub MapfixID:MapfixID,
|
||||
@@ -641,7 +638,7 @@ impl MapfixID{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ActionOperationFailedRequest{
|
||||
pub OperationID:OperationID,
|
||||
@@ -668,7 +665,7 @@ impl Resource{
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Serialize)]
|
||||
pub struct ReleaseInfo{
|
||||
pub SubmissionID:SubmissionID,
|
||||
@@ -678,7 +675,7 @@ pub struct ReleaseInfo{
|
||||
pub struct ReleaseRequest<'a>{
|
||||
pub schedule:&'a [ReleaseInfo],
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(Clone,Debug,serde::Deserialize)]
|
||||
pub struct OperationIDResponse{
|
||||
pub OperationID:OperationID,
|
||||
|
||||
@@ -6,7 +6,7 @@ use heck::{ToSnakeCase,ToTitleCase};
|
||||
use rbx_dom_weak::Instance;
|
||||
use rust_grpc::validator::Check;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ModelInfoDownload(rbx_asset::cloud::GetError),
|
||||
@@ -33,7 +33,7 @@ macro_rules! lazy_regex{
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct CheckRequest{
|
||||
ModelID:u64,
|
||||
SkipChecks:bool,
|
||||
@@ -79,7 +79,7 @@ struct ModeElement{
|
||||
zone:Zone,
|
||||
mode_id:ModeID,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
pub enum IDParseError{
|
||||
NoCaptures,
|
||||
ParseInt(core::num::ParseIntError),
|
||||
@@ -442,7 +442,7 @@ pub struct MapInfoOwned{
|
||||
pub creator:String,
|
||||
pub game_id:GameID,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum IntoMapInfoOwnedError{
|
||||
DisplayName(StringValueError),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::check::CheckListAndVersion;
|
||||
use crate::nats_types::CheckMapfixRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Check(crate::check::Error),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::check::CheckListAndVersion;
|
||||
use crate::nats_types::CheckSubmissionRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Check(crate::check::Error),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::download::download_asset_version;
|
||||
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
CreatorTypeMustBeUser,
|
||||
@@ -17,11 +17,11 @@ impl std::fmt::Display for Error{
|
||||
}
|
||||
impl std::error::Error for Error{}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct CreateRequest{
|
||||
pub ModelID:u64,
|
||||
}
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct CreateResult{
|
||||
pub AssetOwner:u64,
|
||||
pub DisplayName:Option<String>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::nats_types::CreateMapfixRequest;
|
||||
use crate::create::CreateRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Create(crate::create::Error),
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::nats_types::CreateSubmissionRequest;
|
||||
use crate::create::CreateRequest;
|
||||
use crate::rbx_util::GameID;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
Create(crate::create::Error),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ModelLocationDownload(rbx_asset::cloud::GetError),
|
||||
|
||||
@@ -22,7 +22,6 @@ mod validator;
|
||||
mod validate_mapfix;
|
||||
mod validate_submission;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum StartupError{
|
||||
API(tonic::transport::Error),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum HandleMessageError{
|
||||
Messages(async_nats::jetstream::consumer::pull::MessagesError),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Requests are sent from maps-service to validator
|
||||
// Validation invokes the REST api to update the submissions
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateSubmissionRequest{
|
||||
// operation_id is passed back in the response message
|
||||
@@ -18,7 +18,7 @@ pub struct CreateSubmissionRequest{
|
||||
pub Roles:u32,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateMapfixRequest{
|
||||
pub OperationID:u32,
|
||||
@@ -27,7 +27,7 @@ pub struct CreateMapfixRequest{
|
||||
pub Description:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CheckSubmissionRequest{
|
||||
pub SubmissionID:u64,
|
||||
@@ -35,7 +35,7 @@ pub struct CheckSubmissionRequest{
|
||||
pub SkipChecks:bool,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CheckMapfixRequest{
|
||||
pub MapfixID:u64,
|
||||
@@ -43,7 +43,7 @@ pub struct CheckMapfixRequest{
|
||||
pub SkipChecks:bool,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ValidateSubmissionRequest{
|
||||
// submission_id is passed back in the response message
|
||||
@@ -53,7 +53,7 @@ pub struct ValidateSubmissionRequest{
|
||||
pub ValidatedModelID:Option<u64>,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ValidateMapfixRequest{
|
||||
// submission_id is passed back in the response message
|
||||
@@ -64,7 +64,7 @@ pub struct ValidateMapfixRequest{
|
||||
}
|
||||
|
||||
// Create a new map
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UploadSubmissionRequest{
|
||||
pub SubmissionID:u64,
|
||||
@@ -73,7 +73,7 @@ pub struct UploadSubmissionRequest{
|
||||
pub ModelName:String,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UploadMapfixRequest{
|
||||
pub MapfixID:u64,
|
||||
@@ -83,7 +83,7 @@ pub struct UploadMapfixRequest{
|
||||
}
|
||||
|
||||
// Release a new map
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseSubmissionRequest{
|
||||
pub SubmissionID:u64,
|
||||
@@ -97,14 +97,14 @@ pub struct ReleaseSubmissionRequest{
|
||||
pub Submitter:u64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseSubmissionsBatchRequest{
|
||||
pub Submissions:Vec<ReleaseSubmissionRequest>,
|
||||
pub OperationID:u32,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseMapfixRequest{
|
||||
pub MapfixID:u64,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ReadDomError{
|
||||
Binary(rbx_binary::DecodeError),
|
||||
|
||||
@@ -75,25 +75,27 @@ 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,
|
||||
submissions_service:&crate::grpc::submissions::Service,
|
||||
submissions:&crate::grpc::submissions::Service,
|
||||
)->Result<(),InnerError>{
|
||||
let available_parallelism=std::thread::available_parallelism().map_err(InnerError::Io)?.get();
|
||||
// set up futures
|
||||
|
||||
let submissions=&release_info.Submissions;
|
||||
// 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(submissions)
|
||||
.enumerate()
|
||||
.map(|(index,submission)|{
|
||||
let asset_version=rbx_asset::cloud::GetAssetVersionRequest{
|
||||
asset_id:submission.ModelID,
|
||||
version:submission.ModelVersion,
|
||||
};
|
||||
async move{
|
||||
let modes=download_fut(cloud_context,asset_version).await;
|
||||
(index,modes)
|
||||
}
|
||||
let fut_download=futures::stream::iter(asset_versions)
|
||||
.map(|(index,asset_version)|async move{
|
||||
let modes=download_fut(cloud_context,asset_version).await;
|
||||
(index,modes)
|
||||
})
|
||||
.buffer_unordered(available_parallelism.min(MAX_PARALLEL_DECODE))
|
||||
.collect::<Vec<(usize,Result<_,DownloadFutError>)>>();
|
||||
@@ -102,7 +104,7 @@ async fn release_inner(
|
||||
let fut_load_asset_versions=load_asset_versions(
|
||||
cloud_context_luau_execution,
|
||||
load_asset_version_runtime,
|
||||
submissions.iter().map(|submission|submission.UploadedAssetID),
|
||||
release_info.Submissions.iter().map(|submission|submission.UploadedAssetID),
|
||||
);
|
||||
|
||||
// execute futures
|
||||
@@ -111,7 +113,7 @@ async fn release_inner(
|
||||
let load_asset_versions=load_asset_versions_result.map_err(InnerError::LoadAssetVersions)?;
|
||||
|
||||
// sanity check roblox output
|
||||
if load_asset_versions.len()!=submissions.len(){
|
||||
if load_asset_versions.len()!=release_info.Submissions.len(){
|
||||
return Err(InnerError::LoadAssetVersionsListLength);
|
||||
};
|
||||
|
||||
@@ -125,7 +127,7 @@ async fn release_inner(
|
||||
match result{
|
||||
Ok(value)=>modes.push(value),
|
||||
Err(error)=>errors.push(ErrorContext{
|
||||
submission_id:submissions[index].SubmissionID,
|
||||
submission_id:release_info.Submissions[index].SubmissionID,
|
||||
error:error,
|
||||
}),
|
||||
}
|
||||
@@ -142,7 +144,7 @@ async fn release_inner(
|
||||
.zip(modes)
|
||||
.zip(load_asset_versions)
|
||||
.map(|((submission,modes),asset_version)|async move{
|
||||
let result=submissions_service.set_status_released(rust_grpc::validator::SubmissionReleaseRequest{
|
||||
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,
|
||||
@@ -181,7 +183,7 @@ async fn release_inner(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
UpdateOperation(tonic::Status),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::download::download_asset_version;
|
||||
use crate::nats_types::UploadMapfixRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum InnerError{
|
||||
Download(crate::download::Error),
|
||||
@@ -43,7 +43,7 @@ async fn upload_inner(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionMapfixUploaded(tonic::Status),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::download::download_asset_version;
|
||||
use crate::nats_types::UploadSubmissionRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum InnerError{
|
||||
Download(crate::download::Error),
|
||||
@@ -44,7 +44,7 @@ async fn upload_inner(
|
||||
Ok(upload_response.AssetId)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionSubmissionUploaded(tonic::Status),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::nats_types::ValidateMapfixRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionMapfixValidate(tonic::Status),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::nats_types::ValidateSubmissionRequest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ApiActionSubmissionValidate(tonic::Status),
|
||||
|
||||
@@ -17,7 +17,7 @@ fn hash_source(source:&str)->u64{
|
||||
std::hash::Hasher::finish(&hasher)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ModelInfoDownload(rbx_asset::cloud::GetError),
|
||||
@@ -52,7 +52,7 @@ impl std::fmt::Display for Error{
|
||||
}
|
||||
impl std::error::Error for Error{}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
#[expect(nonstandard_style)]
|
||||
pub struct ValidateRequest{
|
||||
pub ModelID:u64,
|
||||
pub ModelVersion:u64,
|
||||
|
||||
34
web/.gitignore
vendored
34
web/.gitignore
vendored
@@ -1,24 +1,12 @@
|
||||
bun.lockb
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -29,12 +17,22 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
# env files
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
FROM registry.itzana.me/docker-proxy/oven/bun:1.3.3
|
||||
# Build stage
|
||||
FROM registry.itzana.me/docker-proxy/oven/bun:1.3.3 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lockb* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
# Release
|
||||
FROM registry.itzana.me/docker-proxy/nginx:alpine
|
||||
|
||||
COPY --from=builder /app/build /usr/share/nginx/html
|
||||
|
||||
# Add nginx configuration for SPA routing
|
||||
RUN echo 'server { \
|
||||
listen 3000; \
|
||||
location / { \
|
||||
root /usr/share/nginx/html; \
|
||||
index index.html; \
|
||||
try_files $uri $uri/ /index.html; \
|
||||
} \
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN bun install
|
||||
RUN bun run build
|
||||
ENTRYPOINT ["bun", "run", "start"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
677
web/bun.lock
677
web/bun.lock
File diff suppressed because it is too large
Load Diff
13
web/index.html
Normal file
13
web/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Maps Service</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
distDir: "build",
|
||||
output: "standalone",
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**.rbxcdn.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
4142
web/package-lock.json
generated
Normal file
4142
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,21 +2,24 @@
|
||||
"name": "map-service-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000 --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3000",
|
||||
"lint": "next lint"
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext ts,tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^7.3.6",
|
||||
"@mui/material": "^7.3.6",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "^16.0.7",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"sass": "^1.94.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -24,8 +27,9 @@
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "16.0.7",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
46
web/src/App.tsx
Normal file
46
web/src/App.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import { ThemeProvider } from '@mui/material'
|
||||
import { theme } from '@/app/lib/theme'
|
||||
|
||||
// Pages
|
||||
import Home from '@/app/page'
|
||||
import MapsPage from '@/app/maps/page'
|
||||
import MapDetailPage from '@/app/maps/[mapId]/page'
|
||||
import MapFixCreatePage from '@/app/maps/[mapId]/fix/page'
|
||||
import MapfixesPage from '@/app/mapfixes/page'
|
||||
import MapfixDetailPage from '@/app/mapfixes/[mapfixId]/page'
|
||||
import SubmissionsPage from '@/app/submissions/page'
|
||||
import SubmissionDetailPage from '@/app/submissions/[submissionId]/page'
|
||||
import SubmitPage from '@/app/submit/page'
|
||||
import AdminSubmitPage from '@/app/admin-submit/page'
|
||||
import OperationPage from '@/app/operations/[operationId]/page'
|
||||
import ReviewerDashboardPage from '@/app/reviewer-dashboard/page'
|
||||
import UserDashboardPage from '@/app/user-dashboard/page'
|
||||
import ScriptReviewPage from '@/app/script-review/page'
|
||||
import NotFound from '@/app/not-found/page'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/maps" element={<MapsPage />} />
|
||||
<Route path="/maps/:mapId" element={<MapDetailPage />} />
|
||||
<Route path="/maps/:mapId/fix" element={<MapFixCreatePage />} />
|
||||
<Route path="/mapfixes" element={<MapfixesPage />} />
|
||||
<Route path="/mapfixes/:mapfixId" element={<MapfixDetailPage />} />
|
||||
<Route path="/submissions" element={<SubmissionsPage />} />
|
||||
<Route path="/submissions/:submissionId" element={<SubmissionDetailPage />} />
|
||||
<Route path="/submit" element={<SubmitPage />} />
|
||||
<Route path="/admin-submit" element={<AdminSubmitPage />} />
|
||||
<Route path="/operations/:operationId" element={<OperationPage />} />
|
||||
<Route path="/review" element={<ReviewerDashboardPage />} />
|
||||
<Route path="/dashboard" element={<UserDashboardPage />} />
|
||||
<Route path="/script-review" element={<ScriptReviewPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Box, IconButton, Typography} from "@mui/material";
|
||||
import {Box, Button, IconButton, Typography} from "@mui/material";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
@@ -65,14 +65,22 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
|
||||
return (
|
||||
<Box mb={6}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Typography variant="h4" component="h2" fontWeight="bold">
|
||||
{title}
|
||||
</Typography>
|
||||
<Link href={viewAllLink} style={{textDecoration: 'none'}}>
|
||||
<Typography component="span" color="primary">
|
||||
View All →
|
||||
</Typography>
|
||||
<Link to={viewAllLink} style={{textDecoration: 'none'}}>
|
||||
<Button
|
||||
endIcon={<ArrowForwardIosIcon sx={{ fontSize: '0.875rem' }} />}
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
@@ -85,9 +93,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
boxShadow: 2,
|
||||
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover',
|
||||
backgroundColor: 'background.paper',
|
||||
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||
},
|
||||
visibility: scrollPosition <= 5 ? 'hidden' : 'visible',
|
||||
}}
|
||||
@@ -106,7 +117,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
gap: '16px', // Fixed 16px gap - using string with px unit to ensure it's absolute
|
||||
gap: '20px',
|
||||
padding: '8px 4px',
|
||||
}}
|
||||
>
|
||||
@@ -116,7 +127,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
sx={{
|
||||
flex: '0 0 auto',
|
||||
width: {
|
||||
xs: '260px', // Fixed width at different breakpoints
|
||||
xs: '260px',
|
||||
sm: '280px',
|
||||
md: '300px'
|
||||
}
|
||||
@@ -135,9 +146,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
boxShadow: 2,
|
||||
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover',
|
||||
backgroundColor: 'background.paper',
|
||||
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||
},
|
||||
visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible',
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
Typography,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Skeleton
|
||||
} from "@mui/material";
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import { formatDistanceToNow, format } from "date-fns";
|
||||
import { AuditEvent, decodeAuditEvent as auditEventMessage } from "@/app/ts/AuditEvent";
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
|
||||
interface AuditEventItemProps {
|
||||
event: AuditEvent;
|
||||
@@ -15,17 +16,44 @@ interface AuditEventItemProps {
|
||||
}
|
||||
|
||||
export default function AuditEventItem({ event, validatorUser }: AuditEventItemProps) {
|
||||
const isValidator = event.User === validatorUser;
|
||||
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar
|
||||
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
|
||||
>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
borderRadius: 1
|
||||
}}>
|
||||
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 40,
|
||||
height: 40,
|
||||
opacity: isLoading ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
animation="wave"
|
||||
/>
|
||||
<Avatar
|
||||
src={isValidator ? undefined : (thumbnailUrl || undefined)}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
opacity: isLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="subtitle2">
|
||||
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
|
||||
{isValidator ? "Validator" : event.Username || "Unknown"}
|
||||
</Typography>
|
||||
<DateDisplay date={event.Date} />
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
@@ -22,18 +21,21 @@ export default function AuditEventsTabPanel({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box role="tabpanel" hidden={activeTab !== 1}>
|
||||
{activeTab === 1 && (
|
||||
<Stack spacing={2}>
|
||||
{filteredEvents.map((event, index) => (
|
||||
<AuditEventItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Box
|
||||
role="tabpanel"
|
||||
sx={{
|
||||
display: activeTab === 1 ? 'block' : 'none'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{filteredEvents.map((event, index) => (
|
||||
<AuditEventItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
Typography,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Skeleton
|
||||
} from "@mui/material";
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import { formatDistanceToNow, format } from "date-fns";
|
||||
import { AuditEvent, decodeAuditEvent } from "@/app/ts/AuditEvent";
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
|
||||
interface CommentItemProps {
|
||||
event: AuditEvent;
|
||||
@@ -15,21 +16,43 @@ interface CommentItemProps {
|
||||
}
|
||||
|
||||
export default function CommentItem({ event, validatorUser }: CommentItemProps) {
|
||||
const isValidator = event.User === validatorUser;
|
||||
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Avatar
|
||||
src={event.User === validatorUser ? undefined : `/thumbnails/user/${event.User}`}
|
||||
>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 40,
|
||||
height: 40,
|
||||
opacity: isLoading ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
animation="wave"
|
||||
/>
|
||||
<Avatar
|
||||
src={isValidator ? undefined : (thumbnailUrl || undefined)}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
opacity: isLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="subtitle2">
|
||||
{event.User === validatorUser ? "Validator" : event.Username || "Unknown"}
|
||||
{isValidator ? "Validator" : event.Username || "Unknown"}
|
||||
</Typography>
|
||||
<DateDisplay date={event.Date} />
|
||||
</Box>
|
||||
<Typography variant="body2">{decodeAuditEvent(event)}</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{decodeAuditEvent(event)}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,22 @@ import {
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
keyframes
|
||||
} from "@mui/material";
|
||||
import CommentsTabPanel from './CommentsTabPanel';
|
||||
import AuditEventsTabPanel from './AuditEventsTabPanel';
|
||||
import { AuditEvent } from "@/app/ts/AuditEvent";
|
||||
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
||||
|
||||
const pulse = keyframes`
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`;
|
||||
|
||||
interface CommentsAndAuditSectionProps {
|
||||
auditEvents: AuditEvent[];
|
||||
@@ -16,6 +28,7 @@ interface CommentsAndAuditSectionProps {
|
||||
handleCommentSubmit: () => void;
|
||||
validatorUser: number;
|
||||
userId: number | null;
|
||||
currentStatus?: number;
|
||||
}
|
||||
|
||||
export default function CommentsAndAuditSection({
|
||||
@@ -25,13 +38,24 @@ export default function CommentsAndAuditSection({
|
||||
handleCommentSubmit,
|
||||
validatorUser,
|
||||
userId,
|
||||
currentStatus,
|
||||
}: CommentsAndAuditSectionProps) {
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
// Check if there's validator feedback for changes requested status
|
||||
// Show badge if status is ChangesRequested and there are validator events
|
||||
const hasValidatorFeedback = currentStatus === 1 && auditEvents.some(event =>
|
||||
event.User === validatorUser &&
|
||||
(
|
||||
event.EventType === AuditEventType.Error ||
|
||||
event.EventType === AuditEventType.CheckList
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3, mt: 3 }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
@@ -41,7 +65,24 @@ export default function CommentsAndAuditSection({
|
||||
aria-label="comments and audit tabs"
|
||||
>
|
||||
<Tab label="Comments" />
|
||||
<Tab label="Audit Events" />
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
Audit Events
|
||||
{hasValidatorFeedback && (
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ff9800',
|
||||
animation: `${pulse} 2s ease-in-out infinite`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
Avatar,
|
||||
TextField,
|
||||
IconButton
|
||||
IconButton,
|
||||
Skeleton
|
||||
} from "@mui/material";
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
||||
import CommentItem from './CommentItem';
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
|
||||
interface CommentsTabPanelProps {
|
||||
activeTab: number;
|
||||
@@ -34,34 +35,35 @@ export default function CommentsTabPanel({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box role="tabpanel" hidden={activeTab !== 0}>
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||
{commentEvents.length > 0 ? (
|
||||
commentEvents.map((event, index) => (
|
||||
<CommentItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
|
||||
No Comments
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{userId !== null && (
|
||||
<CommentInput
|
||||
newComment={newComment}
|
||||
setNewComment={setNewComment}
|
||||
handleCommentSubmit={handleCommentSubmit}
|
||||
userId={userId}
|
||||
<Box
|
||||
role="tabpanel"
|
||||
sx={{
|
||||
display: activeTab === 0 ? 'block' : 'none'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||
{commentEvents.length > 0 ? (
|
||||
commentEvents.map((event, index) => (
|
||||
<CommentItem
|
||||
key={index}
|
||||
event={event}
|
||||
validatorUser={validatorUser}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<Box sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>
|
||||
No Comments
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{userId !== null && (
|
||||
<CommentInput
|
||||
newComment={newComment}
|
||||
setNewComment={setNewComment}
|
||||
handleCommentSubmit={handleCommentSubmit}
|
||||
userId={userId}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -75,11 +77,32 @@ interface CommentInputProps {
|
||||
}
|
||||
|
||||
function CommentInput({ newComment, setNewComment, handleCommentSubmit, userId }: CommentInputProps) {
|
||||
const { thumbnailUrl, isLoading } = useUserThumbnail(userId || undefined, '150x150');
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||
<Avatar
|
||||
src={`/thumbnails/user/${userId}`}
|
||||
/>
|
||||
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 40,
|
||||
height: 40,
|
||||
opacity: isLoading ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
animation="wave"
|
||||
/>
|
||||
<Avatar
|
||||
src={thumbnailUrl || undefined}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
opacity: isLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import Image from "next/image";
|
||||
import { UserInfo } from "@/app/ts/User";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom"
|
||||
import { useState, useRef } from "react";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
@@ -37,7 +34,7 @@ const navItems: HeaderButton[] = [
|
||||
|
||||
function HeaderButton(header: HeaderButton) {
|
||||
return (
|
||||
<Button color="inherit" component={Link} href={header.href}>
|
||||
<Button color="inherit" component={Link} to={header.href}>
|
||||
{header.name}
|
||||
</Button>
|
||||
);
|
||||
@@ -47,14 +44,26 @@ export default function Header() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
const handleLoginClick = () => {
|
||||
window.location.href =
|
||||
"/auth/oauth2/login?redirect=" + window.location.href;
|
||||
const getAuthUrl = () => {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// Production only
|
||||
if (hostname === 'maps.strafes.net') {
|
||||
return 'https://auth.strafes.net';
|
||||
}
|
||||
|
||||
// Default to staging (works for staging.strafes.net and localhost)
|
||||
return 'https://auth.staging.strafes.net';
|
||||
};
|
||||
|
||||
const [valid, setValid] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<UserInfo | null>(null);
|
||||
const handleLoginClick = () => {
|
||||
const authUrl = getAuthUrl();
|
||||
window.location.href = `${authUrl}/oauth2/login?redirect=${window.location.href}`;
|
||||
};
|
||||
|
||||
const { user, isLoggedIn } = useUser();
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [quickLinksAnchor, setQuickLinksAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
@@ -77,60 +86,34 @@ export default function Header() {
|
||||
setQuickLinksAnchor(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function getLoginInfo() {
|
||||
try {
|
||||
const response = await fetch("/api/session/user");
|
||||
|
||||
if (!response.ok) {
|
||||
setValid(false);
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
const isLoggedIn = userData && 'UserID' in userData;
|
||||
|
||||
setValid(isLoggedIn);
|
||||
setUser(isLoggedIn ? userData : null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
setValid(false);
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
getLoginInfo();
|
||||
}, []);
|
||||
|
||||
// Mobile navigation drawer content
|
||||
const drawer = (
|
||||
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
|
||||
<List>
|
||||
{navItems.map((item) => (
|
||||
<ListItem key={item.name} disablePadding>
|
||||
<ListItemButton component={Link} href={item.href} sx={{ textAlign: 'center' }}>
|
||||
<ListItemButton component={Link} to={item.href} sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary={item.name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{valid && user && (
|
||||
{isLoggedIn && user && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} href="/submit" sx={{ textAlign: 'center' }}>
|
||||
<ListItemButton component={Link} to="/submit" sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary="Submit Map" sx={{ color: 'success.main' }} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
{!valid && (
|
||||
{!isLoggedIn && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={handleLoginClick} sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary="Login" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
{valid && user && (
|
||||
{isLoggedIn && user && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} href="/auth" sx={{ textAlign: 'center' }}>
|
||||
<ListItemButton component="a" href={getAuthUrl()} sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary="Manage Account" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
@@ -150,7 +133,7 @@ export default function Header() {
|
||||
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Toolbar sx={{ py: 1 }}>
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
@@ -165,20 +148,144 @@ export default function Header() {
|
||||
|
||||
{/* Desktop navigation */}
|
||||
{!isMobile && (
|
||||
<Box display="flex" flexGrow={1} gap={2} alignItems="center">
|
||||
<Box display="flex" flexGrow={1} gap={1} alignItems="center">
|
||||
{/* Logo/Brand */}
|
||||
<Box
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{
|
||||
mr: 4,
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'@keyframes speedLine': {
|
||||
'0%': {
|
||||
transform: 'translateX(-50px) scaleX(0.5)',
|
||||
opacity: 0,
|
||||
},
|
||||
'40%': {
|
||||
opacity: 0.8,
|
||||
transform: 'translateX(0px) scaleX(1)',
|
||||
},
|
||||
'100%': {
|
||||
opacity: 0,
|
||||
transform: 'translateX(30px) scaleX(0.7)',
|
||||
},
|
||||
},
|
||||
'@keyframes logoReveal': {
|
||||
'0%': {
|
||||
opacity: 0,
|
||||
transform: 'translateX(-10px)',
|
||||
filter: 'blur(2px)',
|
||||
},
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
transform: 'translateX(0px)',
|
||||
filter: 'blur(0px)',
|
||||
},
|
||||
},
|
||||
'&::before, &::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: 'linear-gradient(90deg, transparent 10%, rgba(59, 130, 246, 0.8) 50%, transparent 90%)',
|
||||
pointerEvents: 'none',
|
||||
animation: !hasAnimated.current ? 'speedLine 0.6s ease-out forwards' : 'none',
|
||||
opacity: !hasAnimated.current ? 0 : undefined,
|
||||
},
|
||||
'&::before': {
|
||||
top: '35%',
|
||||
animationDelay: !hasAnimated.current ? '0s' : undefined,
|
||||
},
|
||||
'&::after': {
|
||||
top: '65%',
|
||||
animationDelay: !hasAnimated.current ? '0.08s' : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: 'linear-gradient(90deg, transparent 10%, rgba(139, 92, 246, 0.6) 50%, transparent 90%)',
|
||||
animation: !hasAnimated.current ? 'speedLine 0.6s ease-out forwards' : 'none',
|
||||
animationDelay: !hasAnimated.current ? '0.04s' : '0s',
|
||||
opacity: !hasAnimated.current ? 0 : undefined,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.01em',
|
||||
fontSize: '1.125rem',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
opacity: !hasAnimated.current ? 0 : 1,
|
||||
animation: !hasAnimated.current ? 'logoReveal 0.5s ease-out forwards' : 'none',
|
||||
animationDelay: !hasAnimated.current ? '0.5s' : '0s',
|
||||
}}
|
||||
onAnimationEnd={() => {
|
||||
hasAnimated.current = true;
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
</Typography>
|
||||
</Box>
|
||||
{navItems.map((item) => (
|
||||
<HeaderButton key={item.name} name={item.name} href={item.href} />
|
||||
<Button
|
||||
key={item.name}
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to={item.href}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderRadius: 1.5,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
color: 'text.secondary',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
))}
|
||||
<Box sx={{ flexGrow: 1 }} /> {/* Push quick links to the right */}
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
{/* Quick Links Dropdown */}
|
||||
<Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleQuickLinksOpen}
|
||||
sx={{ textTransform: 'none', fontSize: '0.95rem', px: 1 }}
|
||||
sx={{
|
||||
px: 2,
|
||||
mr: 1,
|
||||
borderRadius: 1.5,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
color: 'text.secondary',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
QUICK LINKS
|
||||
Quick Links
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={quickLinksAnchor}
|
||||
@@ -186,12 +293,20 @@ export default function Header() {
|
||||
onClose={handleQuickLinksClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{quickLinks.map(link => (
|
||||
<MenuItem
|
||||
key={link.name}
|
||||
onClick={handleQuickLinksClose}
|
||||
sx={{ minWidth: 180 }}
|
||||
sx={{
|
||||
minWidth: 200,
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
component="a"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
@@ -209,30 +324,53 @@ export default function Header() {
|
||||
{isMobile && <Box sx={{ flexGrow: 1 }} />}
|
||||
|
||||
{/* Right side of nav */}
|
||||
<Box display="flex" gap={2}>
|
||||
{!isMobile && valid && user && (
|
||||
<Button variant="outlined" color="success" component={Link} href="/submit">
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
{!isMobile && isLoggedIn && user && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to="/submit"
|
||||
sx={{
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
Submit Map
|
||||
</Button>
|
||||
)}
|
||||
{!isMobile && valid && user ? (
|
||||
{!isMobile && isLoggedIn && user ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Button
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
size="small"
|
||||
style={{ textTransform: "none" }}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
borderRadius: 1.5,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
borderColor: 'rgba(59, 130, 246, 0.3)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
<img
|
||||
className="avatar"
|
||||
width={28}
|
||||
height={28}
|
||||
priority={true}
|
||||
src={user.AvatarURL}
|
||||
alt={user.Username}
|
||||
style={{ marginRight: 8 }}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body1">{user.Username}</Typography>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
|
||||
{user.Username}
|
||||
</Typography>
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
@@ -240,38 +378,58 @@ export default function Header() {
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
horizontal: "right",
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem component={Link} href="/auth">
|
||||
Manage
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={getAuthUrl()}
|
||||
sx={{
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
Manage Account
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
) : !isMobile && (
|
||||
<Button color="inherit" onClick={handleLoginClick}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleLoginClick}
|
||||
sx={{
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* In mobile view, display just the avatar if logged in */}
|
||||
{isMobile && valid && user && (
|
||||
{isMobile && isLoggedIn && user && (
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
<Image
|
||||
<img
|
||||
className="avatar"
|
||||
width={28}
|
||||
height={28}
|
||||
priority={true}
|
||||
width={32}
|
||||
height={32}
|
||||
src={user.AvatarURL}
|
||||
alt={user.Username}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -284,10 +442,13 @@ export default function Header() {
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: 240 },
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
width: 240,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from "react";
|
||||
import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Grid, Typography} from "@mui/material";
|
||||
import {Explore, Person2} from "@mui/icons-material";
|
||||
import {Avatar, Box, Card, CardActionArea, CardContent, CardMedia, Divider, Typography, Skeleton} from "@mui/material";
|
||||
import {Explore, Person2, Assignment, Build} from "@mui/icons-material";
|
||||
import {StatusChip} from "@/app/_components/statusChip";
|
||||
import {Link} from "react-router-dom";
|
||||
import {useAssetThumbnail, useUserThumbnail} from "@/app/hooks/useThumbnails";
|
||||
import {useUsername} from "@/app/hooks/useUsername";
|
||||
import { getGameName } from "@/app/utils/games";
|
||||
|
||||
interface MapCardProps {
|
||||
displayName: string;
|
||||
@@ -14,173 +17,176 @@ interface MapCardProps {
|
||||
gameID: number;
|
||||
created: number;
|
||||
type: 'mapfix' | 'submission';
|
||||
showTypeBadge?: boolean;
|
||||
}
|
||||
|
||||
const CARD_WIDTH = 270;
|
||||
|
||||
export function MapCard(props: MapCardProps) {
|
||||
const { thumbnailUrl: assetThumbnail, isLoading: assetLoading } = useAssetThumbnail(props.assetId);
|
||||
const { thumbnailUrl: userThumbnail, isLoading: userLoading } = useUserThumbnail(props.authorId);
|
||||
const { username, isLoading: usernameLoading } = useUsername(props.type === 'mapfix' ? props.authorId : undefined);
|
||||
|
||||
return (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={props.assetId}>
|
||||
<Box sx={{
|
||||
width: CARD_WIDTH,
|
||||
mx: 'auto', // Center the card in its grid cell
|
||||
}}>
|
||||
<Card sx={{
|
||||
width: CARD_WIDTH,
|
||||
height: 340, // Fixed height for all cards
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<CardActionArea
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch'
|
||||
}}
|
||||
href={`/${props.type === 'submission' ? 'submissions' : 'mapfixes'}/${props.id}`}>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={`/thumbnails/asset/${props.assetId}`}
|
||||
alt={props.displayName}
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={`/${props.type === 'submission' ? 'submissions' : 'mapfixes'}/${props.id}`}>
|
||||
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={180}
|
||||
animation="wave"
|
||||
sx={{
|
||||
height: 160, // Fixed height for all images
|
||||
objectFit: 'cover',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: assetLoading ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={assetThumbnail || '/placeholder-map.png'}
|
||||
alt={props.displayName}
|
||||
sx={{
|
||||
height: 180,
|
||||
objectFit: 'cover',
|
||||
opacity: assetLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out, transform 0.3s',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{props.showTypeBadge && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: 12,
|
||||
opacity: assetLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out 0.1s',
|
||||
bgcolor: props.type === 'submission' ? 'primary.main' : 'secondary.main',
|
||||
color: 'white',
|
||||
borderRadius: '50%',
|
||||
width: 32,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: 2
|
||||
}}
|
||||
>
|
||||
{props.type === 'submission' ? <Assignment sx={{ fontSize: '1.1rem' }} /> : <Build sx={{ fontSize: '1.1rem' }} />}
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
opacity: assetLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out 0.1s',
|
||||
}}
|
||||
>
|
||||
<StatusChip status={props.statusID}/>
|
||||
</Box>
|
||||
</Box>
|
||||
<CardContent sx={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
p: 2,
|
||||
width: '100%',
|
||||
}}>
|
||||
<CardContent>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
variant="h6"
|
||||
component="div"
|
||||
sx={{
|
||||
mb: 1,
|
||||
mb: 1.5,
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
lineHeight: '1.3',
|
||||
// Allow text to wrap
|
||||
lineHeight: '1.4',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
minHeight: '2.8em',
|
||||
}}
|
||||
>
|
||||
{props.displayName}
|
||||
</Typography>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
mb: 1.5,
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Explore sx={{
|
||||
mr: 0.75,
|
||||
mt: 0.25,
|
||||
color: 'text.secondary',
|
||||
fontSize: '0.9rem',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
// Allow text to wrap
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
lineHeight: '1.2',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{props.gameID === 1 ? 'Bhop' : props.gameID === 2 ? 'Surf' : props.gameID === 5 ? 'Fly Trials' : props.gameID === 4 ? 'Deathrun' : 'Unknown'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
mb: 1.5,
|
||||
}}>
|
||||
<Person2 sx={{
|
||||
mr: 0.75,
|
||||
mt: 0.25,
|
||||
color: 'text.secondary',
|
||||
fontSize: '0.9rem',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
// Allow text to wrap
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
lineHeight: '1.2',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{props.author}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Explore sx={{ fontSize: '1rem', color: '#6366f1' }} />
|
||||
<Typography variant="body2" color="text.secondary" fontSize="0.875rem">
|
||||
{getGameName(props.gameID)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Person2 sx={{ fontSize: '1rem', color: '#8b5cf6' }} />
|
||||
{props.type === 'mapfix' && usernameLoading ? (
|
||||
<Skeleton variant="text" width={80} />
|
||||
) : (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
fontSize="0.875rem"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{props.type === 'mapfix' && username ? `@${username}` : props.author}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Divider sx={{ my: 1.5 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={28}
|
||||
height={28}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
opacity: userLoading ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
<Avatar
|
||||
src={`/thumbnails/user/${props.authorId}`}
|
||||
src={userThumbnail || undefined}
|
||||
alt={props.author}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
width: 28,
|
||||
height: 28,
|
||||
opacity: userLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
ml: 1,
|
||||
color: 'text.secondary',
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{/*In the future author should be the username of the submitter not the info from the map*/}
|
||||
{props.author} - {new Date(props.created * 1000).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
{new Date(props.created * 1000).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Box } from '@mui/material';
|
||||
import {MapfixInfo } from "@/app/ts/Mapfix";
|
||||
import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles";
|
||||
import {SubmissionInfo} from "@/app/ts/Submission";
|
||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||
|
||||
interface ReviewAction {
|
||||
name: string,
|
||||
action: string,
|
||||
name: string;
|
||||
action: string;
|
||||
confirmTitle?: string;
|
||||
confirmMessage?: string;
|
||||
requiresConfirmation: boolean;
|
||||
}
|
||||
|
||||
interface ReviewButtonsProps {
|
||||
@@ -19,20 +22,102 @@ interface ReviewButtonsProps {
|
||||
}
|
||||
|
||||
const ReviewActions = {
|
||||
Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction,
|
||||
AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction,
|
||||
SubmitUnchecked: {name:"Submit Unchecked", action:"trigger-submit-unchecked"} as ReviewAction,
|
||||
ResetSubmitting: {name:"Reset Submitting",action:"reset-submitting"} as ReviewAction,
|
||||
Revoke: {name:"Revoke",action:"revoke"} as ReviewAction,
|
||||
Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction,
|
||||
Reject: {name:"Reject",action:"reject"} as ReviewAction,
|
||||
Validate: {name:"Validate",action:"retry-validate"} as ReviewAction,
|
||||
ResetValidating: {name:"Reset Validating",action:"reset-validating"} as ReviewAction,
|
||||
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,
|
||||
Submit: {
|
||||
name: "Submit for Review",
|
||||
action: "trigger-submit",
|
||||
confirmTitle: "Submit for Review",
|
||||
confirmMessage: "Are you ready to submit this for review? The model version is locked in once submitted, but you can revoke it later if needed.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
AdminSubmit: {
|
||||
name: "Submit on Behalf of User",
|
||||
action: "trigger-submit",
|
||||
confirmTitle: "Admin Submit",
|
||||
confirmMessage: "This will submit the work as if the original user did it. Continue?",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
SubmitUnchecked: {
|
||||
name: "Approve Without Validation",
|
||||
action: "trigger-submit-unchecked",
|
||||
confirmTitle: "Skip Validation",
|
||||
confirmMessage: "This will approve without running validation checks. Only use this if you're certain the work is correct.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
ResetSubmitting: {
|
||||
name: "Reset Submit Process",
|
||||
action: "reset-submitting",
|
||||
confirmTitle: "Reset Submit",
|
||||
confirmMessage: "This will force-cancel the submission process and return to 'Under Construction' status. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
Revoke: {
|
||||
name: "Revoke",
|
||||
action: "revoke",
|
||||
confirmTitle: "Revoke",
|
||||
confirmMessage: "This will withdraw from review and return to 'Under Construction' status.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
Accept: {
|
||||
name: "Accept & Validate",
|
||||
action: "trigger-validate",
|
||||
confirmTitle: "Accept",
|
||||
confirmMessage: "This will accept and trigger validation. The work will proceed to the next stage.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
Reject: {
|
||||
name: "Reject",
|
||||
action: "reject",
|
||||
confirmTitle: "Reject",
|
||||
confirmMessage: "This will permanently reject. The user will need to create a new one. Are you sure?",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
Validate: {
|
||||
name: "Run Validation",
|
||||
action: "retry-validate",
|
||||
requiresConfirmation: false
|
||||
} as ReviewAction,
|
||||
ResetValidating: {
|
||||
name: "Reset Validation Process",
|
||||
action: "reset-validating",
|
||||
confirmTitle: "Reset Validation",
|
||||
confirmMessage: "This will force-abort the validation process so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
RequestChanges: {
|
||||
name: "Request Changes",
|
||||
action: "request-changes",
|
||||
confirmTitle: "Request Changes",
|
||||
confirmMessage: "Request that the submitter make changes. Make sure you've explained which changes are requested in a comment.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
Upload: {
|
||||
name: "Upload to Roblox",
|
||||
action: "trigger-upload",
|
||||
confirmTitle: "Upload to Roblox Group",
|
||||
confirmMessage: "This will upload the validated work to the Roblox group. Continue?",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
ResetUploading: {
|
||||
name: "Reset Upload Process",
|
||||
action: "reset-uploading",
|
||||
confirmTitle: "Reset Upload",
|
||||
confirmMessage: "This will force-abort the upload to Roblox so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
Release: {
|
||||
name: "Release to Game",
|
||||
action: "trigger-release",
|
||||
confirmTitle: "Release to Game",
|
||||
confirmMessage: "This will make the work available in game. This is the final step!",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
ResetReleasing: {
|
||||
name: "Reset Release Process",
|
||||
action: "reset-releasing",
|
||||
confirmTitle: "Reset Release",
|
||||
confirmMessage: "This will force-abort the release to the game so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
}
|
||||
|
||||
const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
@@ -42,16 +127,46 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
roles,
|
||||
type,
|
||||
}) => {
|
||||
const getVisibleButtons = () => {
|
||||
if (!item || userId === null) return [];
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
open: boolean;
|
||||
action: ReviewAction | null;
|
||||
}>({ open: false, action: null });
|
||||
|
||||
const handleButtonClick = (action: ReviewAction) => {
|
||||
if (action.requiresConfirmation) {
|
||||
setConfirmDialog({ open: true, action });
|
||||
} else {
|
||||
onClick(action.action, item.ID);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (confirmDialog.action) {
|
||||
onClick(confirmDialog.action.action, item.ID);
|
||||
}
|
||||
setConfirmDialog({ open: false, action: null });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setConfirmDialog({ open: false, action: null });
|
||||
};
|
||||
|
||||
const getVisibleButtons = () => {
|
||||
if (!item || userId === null) return { primary: [], secondary: [], submitter: [], reviewer: [], admin: [] };
|
||||
|
||||
// Define a type for the button
|
||||
type ReviewButton = {
|
||||
action: ReviewAction;
|
||||
color: "primary" | "error" | "success" | "info" | "warning";
|
||||
variant?: "contained" | "outlined";
|
||||
isPrimary?: boolean;
|
||||
};
|
||||
|
||||
const buttons: ReviewButton[] = [];
|
||||
const primaryButtons: ReviewButton[] = [];
|
||||
const secondaryButtons: ReviewButton[] = [];
|
||||
const submitterButtons: ReviewButton[] = [];
|
||||
const reviewerButtons: ReviewButton[] = [];
|
||||
const adminButtons: ReviewButton[] = [];
|
||||
|
||||
const is_submitter = userId === item.Submitter;
|
||||
const status = item.StatusID;
|
||||
|
||||
@@ -59,133 +174,215 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload;
|
||||
const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease;
|
||||
|
||||
// Submitter actions
|
||||
if (is_submitter) {
|
||||
if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) {
|
||||
buttons.push({
|
||||
submitterButtons.push({
|
||||
action: ReviewActions.Submit,
|
||||
color: "primary"
|
||||
color: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
|
||||
buttons.push({
|
||||
submitterButtons.push({
|
||||
action: ReviewActions.Revoke,
|
||||
color: "error"
|
||||
color: "warning",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Submitting) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.ResetSubmitting,
|
||||
color: "warning"
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons for review role
|
||||
// Reviewer actions
|
||||
if (hasRole(roles, reviewRole)) {
|
||||
if (status === Status.Submitted && !is_submitter) {
|
||||
buttons.push(
|
||||
{
|
||||
action: ReviewActions.Accept,
|
||||
color: "success"
|
||||
},
|
||||
{
|
||||
action: ReviewActions.Reject,
|
||||
color: "error"
|
||||
}
|
||||
);
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Accept,
|
||||
color: "success"
|
||||
});
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Reject,
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.AcceptedUnvalidated) {
|
||||
buttons.push({
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Validate,
|
||||
color: "info"
|
||||
color: "primary"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Validating) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.ResetValidating,
|
||||
color: "warning"
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||
buttons.push({
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.RequestChanges,
|
||||
color: "warning"
|
||||
color: "warning",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.ChangesRequested) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.SubmitUnchecked,
|
||||
color: "warning"
|
||||
color: "warning",
|
||||
variant: "outlined"
|
||||
});
|
||||
// button only exists for submissions
|
||||
// submitter has normal submit button
|
||||
if (type === "submission" && !is_submitter) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.AdminSubmit,
|
||||
color: "primary"
|
||||
color: "info",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons for upload role
|
||||
// Upload role actions
|
||||
if (hasRole(roles, uploadRole)) {
|
||||
if (status === Status.Validated) {
|
||||
buttons.push({
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Upload,
|
||||
color: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Uploading) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.ResetUploading,
|
||||
color: "warning"
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons for release role
|
||||
// Release role actions
|
||||
if (hasRole(roles, releaseRole)) {
|
||||
// submissions do not have a release button
|
||||
if (type === "mapfix" && status === Status.Uploaded) {
|
||||
buttons.push({
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.Release,
|
||||
color: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Releasing) {
|
||||
buttons.push({
|
||||
adminButtons.push({
|
||||
action: ReviewActions.ResetReleasing,
|
||||
color: "warning"
|
||||
color: "error",
|
||||
variant: "outlined"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return buttons;
|
||||
return {
|
||||
primary: primaryButtons,
|
||||
secondary: secondaryButtons,
|
||||
submitter: submitterButtons,
|
||||
reviewer: reviewerButtons,
|
||||
admin: adminButtons
|
||||
};
|
||||
};
|
||||
|
||||
const buttons = getVisibleButtons();
|
||||
const hasAnyButtons = buttons.submitter.length > 0 || buttons.reviewer.length > 0 || buttons.admin.length > 0;
|
||||
|
||||
if (!hasAnyButtons) return null;
|
||||
|
||||
const ActionCard = ({ title, actions, isFirst = false }: { title: string; actions: any[]; isFirst?: boolean }) => {
|
||||
if (actions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: isFirst ? 0 : 3 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
fontWeight={600}
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
mb: 1.5,
|
||||
display: 'block'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{actions.map((button, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="contained"
|
||||
color={button.color}
|
||||
fullWidth
|
||||
size="large"
|
||||
onClick={() => handleButtonClick(button.action)}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
py: 1.5
|
||||
}}
|
||||
>
|
||||
{button.action.name}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={2} sx={{ mb: 3 }}>
|
||||
{getVisibleButtons().map((button, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="contained"
|
||||
color={button.color}
|
||||
fullWidth
|
||||
onClick={() => onClick(button.action.action, item.ID)}
|
||||
>
|
||||
{button.action.name}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
<>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<ActionCard title="Your Actions" actions={buttons.submitter} isFirst={true} />
|
||||
<ActionCard title="Review Actions" actions={buttons.reviewer} isFirst={buttons.submitter.length === 0} />
|
||||
<ActionCard title="Admin Actions" actions={buttons.admin} isFirst={buttons.submitter.length === 0 && buttons.reviewer.length === 0} />
|
||||
</Box>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={confirmDialog.open}
|
||||
onClose={handleCancel}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle sx={{ pb: 1 }}>
|
||||
{confirmDialog.action?.confirmTitle || confirmDialog.action?.name}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{confirmDialog.action?.confirmMessage || "Are you sure you want to proceed?"}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={handleCancel} color="inherit">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Paper, Grid, Typography } from "@mui/material";
|
||||
import { Paper, Grid, Typography, TextField, IconButton, Box } from "@mui/material";
|
||||
import { ReviewItemHeader } from "./ReviewItemHeader";
|
||||
import { CopyableField } from "@/app/_components/review/CopyableField";
|
||||
import WorkflowStepper from "./WorkflowStepper";
|
||||
import { SubmissionInfo } from "@/app/ts/Submission";
|
||||
import { MapfixInfo } from "@/app/ts/Mapfix";
|
||||
import { getGameName } from "@/app/utils/games";
|
||||
import { useState } from "react";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { Status, StatusMatches } from "@/app/ts/Status";
|
||||
|
||||
// Define a field configuration for specific types
|
||||
interface FieldConfig {
|
||||
@@ -16,12 +23,24 @@ type ReviewItemType = SubmissionInfo | MapfixInfo;
|
||||
interface ReviewItemProps {
|
||||
item: ReviewItemType;
|
||||
handleCopyValue: (value: string) => void;
|
||||
currentUserId?: number;
|
||||
userId?: number | null;
|
||||
onDescriptionUpdate?: () => Promise<void>;
|
||||
showSnackbar?: (message: string, severity?: 'success' | 'error' | 'info' | 'warning') => void;
|
||||
}
|
||||
|
||||
export function ReviewItem({
|
||||
item,
|
||||
handleCopyValue
|
||||
handleCopyValue,
|
||||
currentUserId,
|
||||
userId,
|
||||
onDescriptionUpdate,
|
||||
showSnackbar
|
||||
}: ReviewItemProps) {
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||
const [editedDescription, setEditedDescription] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Type guard to check if item is valid
|
||||
if (!item) return null;
|
||||
|
||||
@@ -29,6 +48,57 @@ export function ReviewItem({
|
||||
const isSubmission = 'UploadedAssetID' in item;
|
||||
const isMapfix = 'TargetAssetID' in item;
|
||||
|
||||
// Check if current user is the submitter
|
||||
const isSubmitter = userId !== null && userId === item.Submitter;
|
||||
|
||||
// Check if description can be edited (only in ChangesRequested or UnderConstruction status)
|
||||
const canEditDescription = isSubmitter && isMapfix && StatusMatches(item.StatusID, [Status.ChangesRequested, Status.UnderConstruction]);
|
||||
|
||||
const handleEditClick = () => {
|
||||
setEditedDescription(isMapfix ? (item.Description || "") : "");
|
||||
setIsEditingDescription(true);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditingDescription(false);
|
||||
setEditedDescription("");
|
||||
};
|
||||
|
||||
const handleSaveDescription = async () => {
|
||||
if (!isMapfix) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await fetch(`/v1/mapfixes/${item.ID}/description`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: editedDescription,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update description: ${response.status}`);
|
||||
}
|
||||
|
||||
setIsEditingDescription(false);
|
||||
if (showSnackbar) {
|
||||
showSnackbar("Description updated successfully", "success");
|
||||
}
|
||||
if (onDescriptionUpdate) {
|
||||
await onDescriptionUpdate();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating description:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to update description";
|
||||
if (showSnackbar) {
|
||||
showSnackbar(errorMessage, "error");
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Define static fields based on item type
|
||||
let fields: FieldConfig[] = [];
|
||||
if (isSubmission) {
|
||||
@@ -46,17 +116,18 @@ export function ReviewItem({
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
|
||||
<ReviewItemHeader
|
||||
displayName={item.DisplayName}
|
||||
assetId={isMapfix ? item.TargetAssetID : undefined}
|
||||
statusId={item.StatusID}
|
||||
creator={item.Creator}
|
||||
submitterId={item.Submitter}
|
||||
/>
|
||||
<>
|
||||
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4 }}>
|
||||
<ReviewItemHeader
|
||||
displayName={item.DisplayName}
|
||||
assetId={isMapfix ? item.TargetAssetID : undefined}
|
||||
statusId={item.StatusID}
|
||||
creator={item.Creator}
|
||||
submitterId={item.Submitter}
|
||||
/>
|
||||
|
||||
{/* Item Details */}
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
{/* Item Details */}
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
{fields.map((field) => {
|
||||
const fieldValue = (item as never)[field.key];
|
||||
const displayValue = fieldValue === 0 || fieldValue == null ? 'N/A' : fieldValue;
|
||||
@@ -74,19 +145,83 @@ export function ReviewItem({
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{/* Description Section */}
|
||||
{isMapfix && item.Description && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
Description
|
||||
<Grid size={{ xs: 12, sm: 6}}>
|
||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||
Game
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{item.Description}
|
||||
{getGameName(item.GameID)}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Description Section */}
|
||||
{isMapfix && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
|
||||
<Typography variant="subtitle1" fontWeight="bold">
|
||||
Description
|
||||
</Typography>
|
||||
{canEditDescription && !isEditingDescription && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleEditClick}
|
||||
sx={{ ml: 1 }}
|
||||
aria-label="edit description"
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
{isEditingDescription ? (
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
value={editedDescription}
|
||||
onChange={(e) => setEditedDescription(e.target.value)}
|
||||
placeholder="Describe the changes made in this mapfix"
|
||||
slotProps={{ htmlInput: { maxLength: 256 } }}
|
||||
helperText={`${editedDescription.length}/256 characters`}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<Box display="flex" gap={1} mt={1}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleSaveDescription}
|
||||
disabled={isSaving}
|
||||
aria-label="save description"
|
||||
>
|
||||
<SaveIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleCancelEdit}
|
||||
disabled={isSaving}
|
||||
aria-label="cancel edit"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body1">
|
||||
{item.Description || "No description provided"}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Workflow Progress Indicator */}
|
||||
<Paper elevation={3} sx={{ p: 3, borderRadius: 2, mb: 4, display: { xs: 'none', md: 'block' } }}>
|
||||
<WorkflowStepper
|
||||
currentStatus={item.StatusID}
|
||||
type={isMapfix ? 'mapfix' : 'submission'}
|
||||
submitterId={item.Submitter}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
import {Typography, Box, Avatar, keyframes} from "@mui/material";
|
||||
import {Typography, Box, Avatar, keyframes, Skeleton} from "@mui/material";
|
||||
import { StatusChip } from "@/app/_components/statusChip";
|
||||
import { SubmissionStatus } from "@/app/ts/Submission";
|
||||
import { MapfixStatus } from "@/app/ts/Mapfix";
|
||||
import {Status, StatusMatches} from "@/app/ts/Status";
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Link } from "react-router-dom";
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
import { useUserThumbnail } from "@/app/hooks/useThumbnails";
|
||||
import { useUsername } from "@/app/hooks/useUsername";
|
||||
|
||||
function SubmitterName({ submitterId }: { submitterId: number }) {
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { username, isLoading } = useUsername(submitterId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!submitterId) return;
|
||||
const fetchUserName = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/proxy/users/${submitterId}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch user');
|
||||
const data = await response.json();
|
||||
setName(`@${data.name}`);
|
||||
} catch {
|
||||
setName(String(submitterId));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchUserName();
|
||||
}, [submitterId]);
|
||||
const displayName = username ? `@${username}` : String(submitterId);
|
||||
|
||||
if (loading) return <Typography variant="body1">Loading...</Typography>;
|
||||
return <Link href={`https://www.roblox.com/users/${submitterId}/profile`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, '&:hover': { textDecoration: 'underline' } }}>
|
||||
<Typography>
|
||||
{name || submitterId}
|
||||
</Typography>
|
||||
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
|
||||
return <a href={`https://www.roblox.com/users/${submitterId}/profile`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, '&:hover': { textDecoration: 'underline' }, position: 'relative' }}>
|
||||
<Skeleton
|
||||
variant="text"
|
||||
width={80}
|
||||
sx={{
|
||||
opacity: isLoading ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
position: isLoading ? 'relative' : 'absolute',
|
||||
}}
|
||||
animation="wave"
|
||||
/>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
opacity: isLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}>
|
||||
<Typography>
|
||||
{displayName}
|
||||
</Typography>
|
||||
<LaunchIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
</a>
|
||||
}
|
||||
|
||||
interface ReviewItemHeaderProps {
|
||||
@@ -49,7 +50,8 @@ interface ReviewItemHeaderProps {
|
||||
}
|
||||
|
||||
export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, submitterId }: ReviewItemHeaderProps) => {
|
||||
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]);
|
||||
const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting, Status.Releasing]);
|
||||
const { thumbnailUrl, isLoading } = useUserThumbnail(submitterId, '150x150');
|
||||
const pulse = keyframes`
|
||||
0%, 100% { opacity: 0.2; transform: scale(0.8); }
|
||||
50% { opacity: 1; transform: scale(1); }
|
||||
@@ -59,7 +61,7 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
|
||||
<>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
{assetId != null ? (
|
||||
<Link href={`/maps/${assetId}`} passHref legacyBehavior>
|
||||
<Link to={`/maps/${assetId}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} title="View related map">
|
||||
<Typography
|
||||
variant="h4"
|
||||
@@ -111,10 +113,28 @@ export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, subm
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Avatar
|
||||
src={`/thumbnails/user/${submitterId}`}
|
||||
sx={{ mr: 1, width: 24, height: 24 }}
|
||||
/>
|
||||
<Box sx={{ position: 'relative', mr: 1, width: 24, height: 24 }}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 24,
|
||||
height: 24,
|
||||
opacity: isLoading ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
animation="wave"
|
||||
/>
|
||||
<Avatar
|
||||
src={thumbnailUrl || undefined}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
opacity: isLoading ? 0 : 1,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<SubmitterName submitterId={submitterId} />
|
||||
</Box>
|
||||
</>
|
||||
|
||||
315
web/src/app/_components/review/WorkflowStepper.tsx
Normal file
315
web/src/app/_components/review/WorkflowStepper.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import React from 'react';
|
||||
import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes, Typography, Paper } from '@mui/material';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import PendingIcon from '@mui/icons-material/Pending';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import { Status } from '@/app/ts/Status';
|
||||
|
||||
const pulse = keyframes`
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
interface WorkflowStepperProps {
|
||||
currentStatus: number;
|
||||
type: 'submission' | 'mapfix';
|
||||
submitterId?: number;
|
||||
currentUserId?: number;
|
||||
}
|
||||
|
||||
// Define the workflow steps
|
||||
interface WorkflowStep {
|
||||
label: string;
|
||||
statuses: number[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Transitional states that show as "in progress"
|
||||
const transitionalStates = [
|
||||
Status.Submitting,
|
||||
Status.Validating,
|
||||
Status.Uploading,
|
||||
Status.Releasing
|
||||
];
|
||||
|
||||
const submissionWorkflow: WorkflowStep[] = [
|
||||
{
|
||||
label: 'Draft',
|
||||
statuses: [Status.UnderConstruction, Status.ChangesRequested],
|
||||
description: 'Creating or revising'
|
||||
},
|
||||
{
|
||||
label: 'Submitted',
|
||||
statuses: [Status.Submitting, Status.Submitted],
|
||||
description: 'Awaiting review'
|
||||
},
|
||||
{
|
||||
label: 'Accepted',
|
||||
statuses: [Status.AcceptedUnvalidated],
|
||||
description: 'Script review pending'
|
||||
},
|
||||
{
|
||||
label: 'Validated',
|
||||
statuses: [Status.Validating, Status.Validated],
|
||||
description: 'Scripts approved'
|
||||
},
|
||||
{
|
||||
label: 'Uploaded',
|
||||
statuses: [Status.Uploading, Status.Uploaded],
|
||||
description: 'Published to Roblox group'
|
||||
},
|
||||
{
|
||||
label: 'Released',
|
||||
statuses: [Status.Releasing, Status.Release],
|
||||
description: 'Live in-game'
|
||||
}
|
||||
];
|
||||
|
||||
const mapfixWorkflow: WorkflowStep[] = [
|
||||
{
|
||||
label: 'Draft',
|
||||
statuses: [Status.UnderConstruction, Status.ChangesRequested],
|
||||
description: 'Creating or revising'
|
||||
},
|
||||
{
|
||||
label: 'Submitted',
|
||||
statuses: [Status.Submitting, Status.Submitted],
|
||||
description: 'Awaiting review'
|
||||
},
|
||||
{
|
||||
label: 'Accepted',
|
||||
statuses: [Status.AcceptedUnvalidated],
|
||||
description: 'Script review pending'
|
||||
},
|
||||
{
|
||||
label: 'Validated',
|
||||
statuses: [Status.Validating, Status.Validated],
|
||||
description: 'Scripts approved'
|
||||
},
|
||||
{
|
||||
label: 'Uploaded',
|
||||
statuses: [Status.Uploading, Status.Uploaded],
|
||||
description: 'Published to Roblox group'
|
||||
},
|
||||
{
|
||||
label: 'Released',
|
||||
statuses: [Status.Releasing, Status.Release],
|
||||
description: 'Live in-game'
|
||||
}
|
||||
];
|
||||
|
||||
const CustomConnector = styled(StepConnector)(({ theme }) => ({
|
||||
[`&.${stepConnectorClasses.alternativeLabel}`]: {
|
||||
top: 10,
|
||||
left: 'calc(-50% + 16px)',
|
||||
right: 'calc(50% + 16px)',
|
||||
},
|
||||
[`&.${stepConnectorClasses.active}`]: {
|
||||
[`& .${stepConnectorClasses.line}`]: {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
[`&.${stepConnectorClasses.completed}`]: {
|
||||
[`& .${stepConnectorClasses.line}`]: {
|
||||
borderColor: theme.palette.success.main,
|
||||
},
|
||||
},
|
||||
[`& .${stepConnectorClasses.line}`]: {
|
||||
borderColor: theme.palette.mode === 'dark' ? theme.palette.grey[800] : '#eaeaf0',
|
||||
borderTopWidth: 3,
|
||||
borderRadius: 1,
|
||||
transition: 'border-color 0.4s ease-in-out',
|
||||
},
|
||||
}));
|
||||
|
||||
const CustomStepIcon = (props: StepIconProps & { isRejected?: boolean; isChangesRequested?: boolean }) => {
|
||||
const { active, completed, className, isRejected, isChangesRequested } = props;
|
||||
|
||||
const iconStyle = {
|
||||
transition: 'color 0.4s ease-in-out, opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
|
||||
};
|
||||
|
||||
if (isRejected) {
|
||||
return <CancelIcon className={className} sx={{ ...iconStyle, color: 'error.main' }} />;
|
||||
}
|
||||
|
||||
if (completed) {
|
||||
return <CheckCircleIcon className={className} sx={{ ...iconStyle, color: 'success.main' }} />;
|
||||
}
|
||||
|
||||
if (active && isChangesRequested) {
|
||||
return <WarningIcon className={className} sx={{ ...iconStyle, color: 'warning.main' }} />;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
return <PendingIcon className={className} sx={{ ...iconStyle, color: 'primary.main', animation: `${pulse} 2s ease-in-out infinite` }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
border: 2,
|
||||
borderColor: 'grey.400',
|
||||
backgroundColor: 'background.paper',
|
||||
transition: 'all 0.4s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type, submitterId, currentUserId }) => {
|
||||
const workflow = type === 'mapfix' ? mapfixWorkflow : submissionWorkflow;
|
||||
|
||||
// Check if rejected or released
|
||||
const isRejected = currentStatus === Status.Rejected;
|
||||
const isReleased = currentStatus === Status.Release || currentStatus === Status.Releasing;
|
||||
const isChangesRequested = currentStatus === Status.ChangesRequested;
|
||||
const isUnderConstruction = currentStatus === Status.UnderConstruction;
|
||||
|
||||
// Find the active step
|
||||
const activeStep = workflow.findIndex(step =>
|
||||
step.statuses.includes(currentStatus)
|
||||
);
|
||||
|
||||
// Determine nudge message
|
||||
const getNudgeContent = () => {
|
||||
if (isUnderConstruction) {
|
||||
return {
|
||||
icon: InfoOutlinedIcon,
|
||||
title: 'Not Yet Submitted',
|
||||
message: 'Your submission has been created but has not been submitted. Click "Submit" to submit it.',
|
||||
color: '#2196f3',
|
||||
bgColor: 'rgba(33, 150, 243, 0.08)'
|
||||
};
|
||||
}
|
||||
if (isChangesRequested) {
|
||||
return {
|
||||
icon: WarningIcon,
|
||||
title: 'Changes Requested',
|
||||
message: 'Review comments and audit events, make modifications, and submit again.',
|
||||
color: '#ff9800',
|
||||
bgColor: 'rgba(255, 152, 0, 0.08)'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const nudge = getNudgeContent();
|
||||
|
||||
// Only show nudge if current user is the submitter
|
||||
const isSubmitter = submitterId !== undefined && currentUserId !== undefined && submitterId === currentUserId;
|
||||
const shouldShowNudge = nudge && isSubmitter;
|
||||
|
||||
// If rejected, show all steps as incomplete with error state
|
||||
if (isRejected) {
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Stepper activeStep={-1} alternativeLabel connector={<CustomConnector />}>
|
||||
{workflow.map((step) => (
|
||||
<Step key={step.label} completed={false}>
|
||||
<StepLabel
|
||||
StepIconComponent={(props) => <CustomStepIcon {...props} isRejected={true} />}
|
||||
error={true}
|
||||
>
|
||||
<Box sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
|
||||
{step.label}
|
||||
</Box>
|
||||
<Box sx={{ fontSize: '0.75rem', color: 'error.main', mt: 0.5 }}>
|
||||
Rejected
|
||||
</Box>
|
||||
</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Stepper activeStep={activeStep} alternativeLabel connector={<CustomConnector />}>
|
||||
{workflow.map((step, index) => {
|
||||
const stepIncludesCurrentStatus = index === activeStep;
|
||||
const isTransitional = transitionalStates.includes(currentStatus);
|
||||
|
||||
// Show as active if in a transitional state OR if changes requested
|
||||
const isActive = stepIncludesCurrentStatus && (isTransitional || isChangesRequested);
|
||||
|
||||
const isCompleted = isReleased
|
||||
? true
|
||||
: index < activeStep || (stepIncludesCurrentStatus && !isTransitional && !isChangesRequested);
|
||||
|
||||
return (
|
||||
<Step key={step.label} completed={isCompleted}>
|
||||
<StepLabel
|
||||
StepIconComponent={(props) => <CustomStepIcon {...props} isChangesRequested={stepIncludesCurrentStatus && isChangesRequested} />}
|
||||
>
|
||||
<Box sx={{
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: isReleased ? 500 : (isActive ? 600 : 500),
|
||||
transition: 'all 0.4s ease-in-out'
|
||||
}}>
|
||||
{step.label}
|
||||
</Box>
|
||||
{step.description && (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: '0.75rem',
|
||||
color: isReleased ? 'text.secondary' : (stepIncludesCurrentStatus && isChangesRequested ? 'warning.main' : (isActive ? 'primary.main' : 'text.secondary')),
|
||||
mt: 0.5,
|
||||
transition: 'color 0.4s ease-in-out'
|
||||
}}
|
||||
>
|
||||
{step.description}
|
||||
</Box>
|
||||
)}
|
||||
</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
|
||||
{/* Action Nudge */}
|
||||
{shouldShowNudge && (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mt: 3,
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
borderLeft: 4,
|
||||
borderColor: nudge.color,
|
||||
backgroundColor: nudge.bgColor,
|
||||
display: 'flex',
|
||||
gap: 1.5,
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ color: nudge.color, display: 'flex', alignItems: 'center', pt: 0.25 }}>
|
||||
<nudge.icon sx={{ fontSize: 24 }} />
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" fontWeight={600} sx={{ color: nudge.color, mb: 0.5 }}>
|
||||
{nudge.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
|
||||
{nudge.message}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowStepper;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {JSX} from "react";
|
||||
import {JSX} from "react";
|
||||
import {Cancel, CheckCircle, Pending} from "@mui/icons-material";
|
||||
import {Chip} from "@mui/material";
|
||||
|
||||
export const StatusChip = ({status}: { status: number }) => {
|
||||
export const StatusChip = ({status}: { status: number }): JSX.Element => {
|
||||
let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
|
||||
let icon: JSX.Element = <Pending fontSize="small"/>;
|
||||
let label: string = 'Unknown';
|
||||
@@ -81,12 +81,6 @@ export const StatusChip = ({status}: { status: number }) => {
|
||||
label={label}
|
||||
color={color}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 24,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import Header from "./header";
|
||||
|
||||
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../globals.scss";
|
||||
@use "../globals.scss";
|
||||
|
||||
::placeholder {
|
||||
color: var(--placeholder-text)
|
||||
@@ -47,8 +47,4 @@ header h1 {
|
||||
form {
|
||||
display: grid;
|
||||
gap: 25px;
|
||||
|
||||
fieldset {
|
||||
border: blue
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import { Button, TextField } from "@mui/material"
|
||||
|
||||
import GameSelection from "./_game";
|
||||
@@ -8,7 +6,7 @@ import Webpage from "@/app/_components/webpage"
|
||||
import React, { useState } from "react";
|
||||
import {useTitle} from "@/app/hooks/useTitle";
|
||||
|
||||
import "./(styles)/page.scss"
|
||||
import "./page.scss"
|
||||
|
||||
interface SubmissionPayload {
|
||||
AssetID: number;
|
||||
@@ -43,7 +41,7 @@ export default function SubmissionInfoPage() {
|
||||
|
||||
try {
|
||||
// Send the POST request
|
||||
const response = await fetch("/api/submissions-admin", {
|
||||
const response = await fetch("/v1/submissions-admin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
|
||||
@@ -8,39 +8,23 @@ $form-label-fontsize: 1.3rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
color-scheme: dark;
|
||||
|
||||
--header-height: 45px;
|
||||
|
||||
--page: white;
|
||||
--page: rgb(15,15,15);
|
||||
--header-grad-left: #363b40;
|
||||
--header-grad-right: #353a40;
|
||||
--header-button-left: white;
|
||||
--header-button-right: #b4b4b4;
|
||||
--header-button-hover: white;
|
||||
--review-border: #c8c8c8;
|
||||
--text-color: #1e1e1e;
|
||||
--review-border: rgb(50,50,50);
|
||||
--text-color: rgb(230,230,230);
|
||||
--anchor-link-review: #008fd6;
|
||||
--window-header: #f5f5f5;
|
||||
--window-header: rgb(10,10,10);
|
||||
--comment-highlighted: #ffffd7;
|
||||
--comment-area: white;
|
||||
--placeholder-text: rgb(150,150,150);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--page: rgb(15,15,15);
|
||||
--header-grad-left: #363b40;
|
||||
--header-grad-right: #353a40;
|
||||
--header-button-left: white;
|
||||
--header-button-right: #b4b4b4;
|
||||
--header-button-hover: white;
|
||||
--review-border: rgb(50,50,50);
|
||||
--text-color: rgb(230,230,230);
|
||||
--anchor-link-review: #008fd6;
|
||||
--window-header: rgb(10,10,10);
|
||||
--comment-highlighted: #ffffd7;
|
||||
--comment-area: rgb(20,20,20);
|
||||
--placeholder-text: rgb(80,80,80);
|
||||
}
|
||||
--comment-area: rgb(20,20,20);
|
||||
--placeholder-text: rgb(80,80,80);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -40,11 +40,11 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
|
||||
try {
|
||||
const [reviewData, auditData] = await Promise.all([
|
||||
fetch(`/api/${itemType}/${itemId}`).then(res => {
|
||||
fetch(`/v1/${itemType}/${itemId}`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${itemType.slice(0, -1)}: ${res.status}`);
|
||||
return res.json();
|
||||
}),
|
||||
fetch(`/api/${itemType}/${itemId}/audit-events?Page=1&Limit=100`).then(res => {
|
||||
fetch(`/v1/${itemType}/${itemId}/audit-events?Page=1&Limit=100`).then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch audit events: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
@@ -58,7 +58,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
}
|
||||
|
||||
try {
|
||||
const rolesResponse = await fetch("/api/session/roles");
|
||||
const rolesResponse = await fetch("/v1/session/roles");
|
||||
if (rolesResponse.ok) {
|
||||
const rolesData = await rolesResponse.json();
|
||||
setRoles(rolesData.Roles);
|
||||
@@ -72,7 +72,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
}
|
||||
|
||||
try {
|
||||
const userResponse = await fetch("/api/session/user");
|
||||
const userResponse = await fetch("/v1/session/user");
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json();
|
||||
setUser(userData.UserID);
|
||||
@@ -100,7 +100,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating])) {
|
||||
if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating, Status.Releasing])) {
|
||||
const intervalId = setInterval(() => {
|
||||
fetchData(true);
|
||||
}, 5000);
|
||||
|
||||
216
web/src/app/hooks/useThumbnails.ts
Normal file
216
web/src/app/hooks/useThumbnails.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type ThumbnailSize = '150x150' | '420x420' | '768x432';
|
||||
|
||||
interface ThumbnailBatchResponse {
|
||||
thumbnails: Record<string, string>;
|
||||
}
|
||||
|
||||
// Batching queue
|
||||
class ThumbnailBatcher {
|
||||
private assetQueue: Map<number, Set<(url: string | null) => void>> = new Map();
|
||||
private userQueue: Map<number, Set<(url: string | null) => void>> = new Map();
|
||||
private assetTimeoutId: NodeJS.Timeout | null = null;
|
||||
private userTimeoutId: NodeJS.Timeout | null = null;
|
||||
private batchDelay = 50; // 50ms delay to collect requests
|
||||
|
||||
async fetchAssetBatch(ids: number[], size: ThumbnailSize): Promise<Record<number, string>> {
|
||||
try {
|
||||
const response = await fetch('/v1/thumbnails/assets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ assetIds: ids, size }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch thumbnails: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: ThumbnailBatchResponse = await response.json();
|
||||
|
||||
// Convert string keys back to numbers
|
||||
const result: Record<number, string> = {};
|
||||
Object.entries(data.thumbnails || {}).forEach(([key, value]) => {
|
||||
result[parseInt(key, 10)] = value;
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching asset thumbnails:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async fetchUserBatch(ids: number[], size: ThumbnailSize): Promise<Record<number, string>> {
|
||||
try {
|
||||
const response = await fetch('/v1/thumbnails/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userIds: ids, size }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch user thumbnails: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: ThumbnailBatchResponse = await response.json();
|
||||
|
||||
// Convert string keys back to numbers
|
||||
const result: Record<number, string> = {};
|
||||
Object.entries(data.thumbnails || {}).forEach(([key, value]) => {
|
||||
result[parseInt(key, 10)] = value;
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user thumbnails:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
queueAssetRequest(id: number, size: ThumbnailSize, callback: (url: string | null) => void) {
|
||||
if (!this.assetQueue.has(id)) {
|
||||
this.assetQueue.set(id, new Set());
|
||||
}
|
||||
this.assetQueue.get(id)!.add(callback);
|
||||
|
||||
if (this.assetTimeoutId) {
|
||||
clearTimeout(this.assetTimeoutId);
|
||||
}
|
||||
|
||||
this.assetTimeoutId = setTimeout(() => {
|
||||
this.flushAssetQueue(size);
|
||||
}, this.batchDelay);
|
||||
}
|
||||
|
||||
queueUserRequest(id: number, size: ThumbnailSize, callback: (url: string | null) => void) {
|
||||
if (!this.userQueue.has(id)) {
|
||||
this.userQueue.set(id, new Set());
|
||||
}
|
||||
this.userQueue.get(id)!.add(callback);
|
||||
|
||||
if (this.userTimeoutId) {
|
||||
clearTimeout(this.userTimeoutId);
|
||||
}
|
||||
|
||||
this.userTimeoutId = setTimeout(() => {
|
||||
this.flushUserQueue(size);
|
||||
}, this.batchDelay);
|
||||
}
|
||||
|
||||
private async flushAssetQueue(size: ThumbnailSize) {
|
||||
if (this.assetQueue.size === 0) return;
|
||||
|
||||
const ids = Array.from(this.assetQueue.keys());
|
||||
const callbacks = new Map(this.assetQueue);
|
||||
this.assetQueue.clear();
|
||||
|
||||
// Split into batches of 100 (API limit)
|
||||
for (let i = 0; i < ids.length; i += 100) {
|
||||
const batchIds = ids.slice(i, i + 100);
|
||||
const results = await this.fetchAssetBatch(batchIds, size);
|
||||
|
||||
batchIds.forEach((id) => {
|
||||
const url = results[id] || null;
|
||||
const cbs = callbacks.get(id);
|
||||
if (cbs) {
|
||||
cbs.forEach((cb) => cb(url));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async flushUserQueue(size: ThumbnailSize) {
|
||||
if (this.userQueue.size === 0) return;
|
||||
|
||||
const ids = Array.from(this.userQueue.keys());
|
||||
const callbacks = new Map(this.userQueue);
|
||||
this.userQueue.clear();
|
||||
|
||||
// Split into batches of 100 (API limit)
|
||||
for (let i = 0; i < ids.length; i += 100) {
|
||||
const batchIds = ids.slice(i, i + 100);
|
||||
const results = await this.fetchUserBatch(batchIds, size);
|
||||
|
||||
batchIds.forEach((id) => {
|
||||
const url = results[id] || null;
|
||||
const cbs = callbacks.get(id);
|
||||
if (cbs) {
|
||||
cbs.forEach((cb) => cb(url));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton batcher instance
|
||||
const batcher = new ThumbnailBatcher();
|
||||
|
||||
/**
|
||||
* Hook to fetch a single asset thumbnail with automatic batching
|
||||
*/
|
||||
export function useAssetThumbnail(assetId: number | undefined, size: ThumbnailSize = '420x420') {
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!assetId) {
|
||||
setThumbnailUrl(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
batcher.queueAssetRequest(assetId, size, (url) => {
|
||||
setThumbnailUrl(url);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [assetId, size]);
|
||||
|
||||
return { thumbnailUrl, isLoading };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch a single user avatar thumbnail with automatic batching
|
||||
*/
|
||||
export function useUserThumbnail(userId: number | undefined, size: ThumbnailSize = '150x150') {
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
setThumbnailUrl(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
batcher.queueUserRequest(userId, size, (url) => {
|
||||
setThumbnailUrl(url);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [userId, size]);
|
||||
|
||||
return { thumbnailUrl, isLoading };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to prefetch multiple thumbnails (useful for lists)
|
||||
* This ensures they're batched together
|
||||
*/
|
||||
export function usePrefetchThumbnails(
|
||||
items: Array<{ assetId?: number; userId?: number }>,
|
||||
assetSize: ThumbnailSize = '420x420',
|
||||
userSize: ThumbnailSize = '150x150'
|
||||
) {
|
||||
useEffect(() => {
|
||||
items.forEach((item) => {
|
||||
if (item.assetId) {
|
||||
batcher.queueAssetRequest(item.assetId, assetSize, () => {});
|
||||
}
|
||||
if (item.userId) {
|
||||
batcher.queueUserRequest(item.userId, userSize, () => {});
|
||||
}
|
||||
});
|
||||
}, [items, assetSize, userSize]);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useTitle(title: string) {
|
||||
|
||||
39
web/src/app/hooks/useUser.ts
Normal file
39
web/src/app/hooks/useUser.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { UserInfo } from '@/app/ts/User';
|
||||
|
||||
async function fetchUser(): Promise<UserInfo | null> {
|
||||
try {
|
||||
const response = await fetch('/v1/session/user');
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
|
||||
if (userData && 'UserID' in userData) {
|
||||
return userData as UserInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
const { data: user, isLoading, error } = useQuery({
|
||||
queryKey: ['user'],
|
||||
queryFn: fetchUser,
|
||||
staleTime: Infinity, // User data won't go stale unless manually invalidated
|
||||
gcTime: Infinity, // Keep in cache indefinitely
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isLoggedIn: user !== null && user !== undefined,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
103
web/src/app/hooks/useUsername.ts
Normal file
103
web/src/app/hooks/useUsername.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface UserBatchResponse {
|
||||
usernames: Record<string, string>;
|
||||
}
|
||||
|
||||
// Batching queue
|
||||
class UserBatcher {
|
||||
private queue: Map<number, Set<(name: string | null) => void>> = new Map();
|
||||
private timeoutId: NodeJS.Timeout | null = null;
|
||||
private batchDelay = 50; // 50ms delay to collect requests
|
||||
|
||||
async fetchBatch(ids: number[]): Promise<Record<number, string>> {
|
||||
try {
|
||||
const response = await fetch('/v1/usernames', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userIds: ids }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch usernames: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: UserBatchResponse = await response.json();
|
||||
|
||||
// Convert string keys back to numbers
|
||||
const result: Record<number, string> = {};
|
||||
Object.entries(data.usernames || {}).forEach(([key, value]) => {
|
||||
result[parseInt(key, 10)] = value;
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching usernames:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
queueRequest(id: number, callback: (name: string | null) => void) {
|
||||
if (!this.queue.has(id)) {
|
||||
this.queue.set(id, new Set());
|
||||
}
|
||||
this.queue.get(id)!.add(callback);
|
||||
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
this.timeoutId = setTimeout(() => {
|
||||
this.flushQueue();
|
||||
}, this.batchDelay);
|
||||
}
|
||||
|
||||
private async flushQueue() {
|
||||
if (this.queue.size === 0) return;
|
||||
|
||||
const ids = Array.from(this.queue.keys());
|
||||
const callbacks = new Map(this.queue);
|
||||
this.queue.clear();
|
||||
|
||||
// Split into batches of 100 (API limit)
|
||||
for (let i = 0; i < ids.length; i += 100) {
|
||||
const batchIds = ids.slice(i, i + 100);
|
||||
const results = await this.fetchBatch(batchIds);
|
||||
|
||||
batchIds.forEach((id) => {
|
||||
const name = results[id] || null;
|
||||
const cbs = callbacks.get(id);
|
||||
if (cbs) {
|
||||
cbs.forEach((cb) => cb(name));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton batcher instance
|
||||
const batcher = new UserBatcher();
|
||||
|
||||
/**
|
||||
* Hook to fetch a single username with automatic batching
|
||||
*/
|
||||
export function useUsername(userId: number | undefined) {
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
setUsername(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
batcher.queueRequest(userId, (name) => {
|
||||
setUsername(name);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
return { username, isLoading };
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import "./globals.scss";
|
||||
import {theme} from "@/app/lib/theme";
|
||||
import {ThemeProvider} from "@mui/material";
|
||||
|
||||
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<ThemeProvider theme={theme}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user