Compare commits
7 Commits
master
...
feature/ao
| Author | SHA1 | Date | |
|---|---|---|---|
| b31ba5d278 | |||
| 29cbab545f | |||
| 60ef6a5df6 | |||
| 1688178cd3 | |||
| ab5f1289c4 | |||
| 2c4627d467 | |||
| 34017ee771 |
39
.drone.yml
39
.drone.yml
@@ -24,7 +24,7 @@ steps:
|
||||
- staging
|
||||
|
||||
- name: build-validator
|
||||
image: rust:1.92
|
||||
image: clux/muslrust:1.91.0-stable
|
||||
commands:
|
||||
- make build-validator
|
||||
when:
|
||||
@@ -32,15 +32,6 @@ steps:
|
||||
- master
|
||||
- staging
|
||||
|
||||
- name: build-combobulator
|
||||
image: rust:1.92
|
||||
commands:
|
||||
- make build-combobulator
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
|
||||
- name: build-frontend
|
||||
image: oven/bun:1.3.3
|
||||
commands:
|
||||
@@ -121,29 +112,6 @@ steps:
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: image-combobulator
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: registry.itzana.me
|
||||
repo: registry.itzana.me/strafesnet/maptest-combobulator
|
||||
tags:
|
||||
- ${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
|
||||
- ${DRONE_BRANCH}
|
||||
username:
|
||||
from_secret: REGISTRY_USER
|
||||
password:
|
||||
from_secret: REGISTRY_PASS
|
||||
dockerfile: combobulator/Containerfile
|
||||
context: .
|
||||
depends_on:
|
||||
- build-combobulator
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: deploy
|
||||
image: argoproj/argocd:latest
|
||||
commands:
|
||||
@@ -151,7 +119,6 @@ steps:
|
||||
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-api:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
|
||||
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-frontend:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
|
||||
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-validator:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
|
||||
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-combobulator:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
|
||||
environment:
|
||||
USERNAME:
|
||||
from_secret: ARGO_USER
|
||||
@@ -161,7 +128,6 @@ steps:
|
||||
- image-backend
|
||||
- image-frontend
|
||||
- image-validator
|
||||
- image-combobulator
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
@@ -177,13 +143,12 @@ steps:
|
||||
depends_on:
|
||||
- build-backend
|
||||
- build-validator
|
||||
- build-combobulator
|
||||
- build-frontend
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
---
|
||||
kind: signature
|
||||
hmac: a654fea05ccf642bb3a41ce777808ff995c8bd7286f2403fae179ce0db025619
|
||||
hmac: 6de9d4b91f14b30561856daf275d1fd523e1ce7a5a3651b660f0d8907b4692fb
|
||||
|
||||
...
|
||||
|
||||
2973
Cargo.lock
generated
2973
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -1,17 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"combobulator",
|
||||
"validation",
|
||||
"submissions-api-rs",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
async-nats = "0.46.0"
|
||||
futures-util = "0.3.31"
|
||||
rbx_asset = { version = "0.5.0", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" }
|
||||
rbx_binary = "2.0.1"
|
||||
rbx_dom_weak = "4.1.0"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||
|
||||
17
Makefile
17
Makefile
@@ -7,17 +7,14 @@ build-backend:
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o build/server cmd/maps-service/service.go
|
||||
|
||||
build-validator:
|
||||
cargo build --release --bin maps-validation
|
||||
|
||||
build-combobulator:
|
||||
cargo build --release --bin maps-combobulator
|
||||
cargo build --release --target x86_64-unknown-linux-musl --bin maps-validation
|
||||
|
||||
build-frontend:
|
||||
rm -rf web/build
|
||||
cd web && bun install --frozen-lockfile
|
||||
cd web && bun run build
|
||||
|
||||
build: build-backend build-validator build-combobulator build-frontend
|
||||
build: build-backend build-validator build-frontend
|
||||
|
||||
# image
|
||||
image-backend:
|
||||
@@ -26,9 +23,6 @@ image-backend:
|
||||
image-validator:
|
||||
docker build . -f validation/Containerfile -t maptest-validator
|
||||
|
||||
image-combobulator:
|
||||
docker build . -f combobulator/Containerfile -t maptest-combobulator
|
||||
|
||||
image-frontend:
|
||||
docker build web -f web/Containerfile -t maptest-frontend
|
||||
|
||||
@@ -39,12 +33,9 @@ docker-backend:
|
||||
docker-validator:
|
||||
make build-validator
|
||||
make image-validator
|
||||
docker-combobulator:
|
||||
make build-combobulator
|
||||
make image-combobulator
|
||||
docker-frontend:
|
||||
make image-frontend
|
||||
|
||||
docker: docker-backend docker-validator docker-combobulator docker-frontend
|
||||
docker: docker-backend docker-validator docker-frontend
|
||||
|
||||
.PHONY: clean build-backend build-validator build-combobulator build-frontend build image-backend image-validator image-combobulator image-frontend docker-backend docker-validator docker-combobulator docker-frontend docker
|
||||
.PHONY: clean build-backend build-validator build-frontend build image-backend image-validator image-frontend docker-backend docker-validator docker-frontend docker
|
||||
|
||||
@@ -26,6 +26,7 @@ func main() {
|
||||
app.Commands = []*cli.Command{
|
||||
cmds.NewServeCommand(),
|
||||
cmds.NewApiCommand(),
|
||||
cmds.NewAORCommand(),
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "maps-combobulator"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-nats.workspace = true
|
||||
aws-config = { version = "1", features = ["behavior-version-latest"] }
|
||||
aws-sdk-s3 = "1"
|
||||
futures-util.workspace = true
|
||||
map-tool = { version = "3.0.0", registry = "strafesnet", features = ["roblox"], default-features = false }
|
||||
rbx_asset.workspace = true
|
||||
rbx_binary.workspace = true
|
||||
rbx_dom_weak.workspace = true
|
||||
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strafesnet_deferred_loader = { version = "0.6.0", registry = "strafesnet" }
|
||||
strafesnet_rbx_loader = { version = "0.10.0", registry = "strafesnet" }
|
||||
strafesnet_snf = { version = "0.3.2", registry = "strafesnet" }
|
||||
tokio.workspace = true
|
||||
tokio-stream = "0.1"
|
||||
@@ -1,4 +0,0 @@
|
||||
FROM debian:trixie-slim AS runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libssl3t64 ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY /target/release/maps-combobulator /
|
||||
ENTRYPOINT ["/maps-combobulator"]
|
||||
@@ -1,152 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
|
||||
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
|
||||
|
||||
use strafesnet_rbx_loader::mesh::{MeshIndex,MeshType,MeshWithSize};
|
||||
|
||||
// disallow non-static lifetimes
|
||||
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
|
||||
rbx_dom_weak::ustr(s)
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum TextureError{
|
||||
NoTexture,
|
||||
RobloxAssetIdParse(RobloxAssetIdParseErr),
|
||||
}
|
||||
impl std::fmt::Display for TextureError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for TextureError{}
|
||||
impl From<RobloxAssetIdParseErr> for TextureError{
|
||||
fn from(value:RobloxAssetIdParseErr)->Self{
|
||||
Self::RobloxAssetIdParse(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextureLoader{
|
||||
textures:HashMap<RobloxAssetId,Texture>,
|
||||
}
|
||||
impl TextureLoader{
|
||||
pub fn new()->Self{
|
||||
Self{
|
||||
textures:HashMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn insert(&mut self,asset_id:RobloxAssetId,texture:Vec<u8>){
|
||||
self.textures.insert(asset_id,Texture::ImageDDS(texture));
|
||||
}
|
||||
}
|
||||
impl Loader for TextureLoader{
|
||||
type Error=TextureError;
|
||||
type Index<'a>=&'a str;
|
||||
type Resource=Texture;
|
||||
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
|
||||
let asset_id:RobloxAssetId=index.parse()?;
|
||||
let data=self.textures.get(&asset_id).ok_or(TextureError::NoTexture)?.clone();
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum MeshError{
|
||||
NoMesh,
|
||||
RobloxAssetIdParse(RobloxAssetIdParseErr),
|
||||
Mesh(strafesnet_rbx_loader::mesh::Error),
|
||||
Union(strafesnet_rbx_loader::union::Error),
|
||||
DecodeBinary(rbx_binary::DecodeError),
|
||||
OneChildPolicy,
|
||||
MissingInstance,
|
||||
}
|
||||
impl std::fmt::Display for MeshError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for MeshError{}
|
||||
impl From<RobloxAssetIdParseErr> for MeshError{
|
||||
fn from(value:RobloxAssetIdParseErr)->Self{
|
||||
Self::RobloxAssetIdParse(value)
|
||||
}
|
||||
}
|
||||
impl From<strafesnet_rbx_loader::mesh::Error> for MeshError{
|
||||
fn from(value:strafesnet_rbx_loader::mesh::Error)->Self{
|
||||
Self::Mesh(value)
|
||||
}
|
||||
}
|
||||
impl From<strafesnet_rbx_loader::union::Error> for MeshError{
|
||||
fn from(value:strafesnet_rbx_loader::union::Error)->Self{
|
||||
Self::Union(value)
|
||||
}
|
||||
}
|
||||
impl From<rbx_binary::DecodeError> for MeshError{
|
||||
fn from(value:rbx_binary::DecodeError)->Self{
|
||||
Self::DecodeBinary(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MeshLoader{
|
||||
meshes:HashMap<RobloxAssetId,MeshWithSize>,
|
||||
unions:HashMap<RobloxAssetId,rbx_dom_weak::WeakDom>,
|
||||
}
|
||||
impl MeshLoader{
|
||||
pub fn new()->Self{
|
||||
Self{
|
||||
meshes:HashMap::new(),
|
||||
unions:HashMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn insert_mesh(&mut self,asset_id:RobloxAssetId,mesh:MeshWithSize){
|
||||
self.meshes.insert(asset_id,mesh);
|
||||
}
|
||||
pub fn insert_union(&mut self,asset_id:RobloxAssetId,union:rbx_dom_weak::WeakDom){
|
||||
self.unions.insert(asset_id,union);
|
||||
}
|
||||
}
|
||||
impl Loader for MeshLoader{
|
||||
type Error=MeshError;
|
||||
type Index<'a>=MeshIndex<'a>;
|
||||
type Resource=MeshWithSize;
|
||||
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
|
||||
let mesh=match index.mesh_type{
|
||||
MeshType::FileMesh=>{
|
||||
let id:RobloxAssetId=index.content.parse()?;
|
||||
let mesh_with_size=self.meshes.get(&id).ok_or(MeshError::NoMesh)?;
|
||||
mesh_with_size.clone()
|
||||
},
|
||||
MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{
|
||||
// decode asset
|
||||
let size=size_float_bits.map(f32::from_bits).into();
|
||||
if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){
|
||||
let id:RobloxAssetId=index.content.parse()?;
|
||||
let dom=self.unions.get(&id).ok_or(MeshError::NoMesh)?;
|
||||
let &[referent]=dom.root().children()else{
|
||||
return Err(MeshError::OneChildPolicy);
|
||||
};
|
||||
let Some(instance)=dom.get_by_ref(referent)else{
|
||||
return Err(MeshError::MissingInstance);
|
||||
};
|
||||
if physics_data.is_empty(){
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("PhysicsData")){
|
||||
physics_data=data.as_ref();
|
||||
}
|
||||
}
|
||||
if mesh_data.is_empty(){
|
||||
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){
|
||||
mesh_data=data.as_ref();
|
||||
}
|
||||
}
|
||||
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
}else{
|
||||
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(mesh)
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
mod loader;
|
||||
mod nats_types;
|
||||
mod process;
|
||||
mod s3;
|
||||
|
||||
const SUBJECT_MAPFIX_RELEASE:&str="maptest.mapfixes.release";
|
||||
const SUBJECT_SUBMISSION_BATCHRELEASE:&str="maptest.submissions.batchrelease";
|
||||
const SUBJECT_SEED:&str="maptest.combobulator.seed";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StartupError{
|
||||
NatsConnect(async_nats::ConnectError),
|
||||
NatsGetStream(async_nats::jetstream::context::GetStreamError),
|
||||
NatsConsumer(async_nats::jetstream::stream::ConsumerError),
|
||||
NatsConsumerUpdate(async_nats::jetstream::stream::ConsumerUpdateError),
|
||||
NatsStream(async_nats::jetstream::consumer::StreamError),
|
||||
}
|
||||
impl std::fmt::Display for StartupError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for StartupError{}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum HandleMessageError{
|
||||
Json(serde_json::Error),
|
||||
UnknownSubject(String),
|
||||
Process(process::Error),
|
||||
Ack(async_nats::Error),
|
||||
Publish(async_nats::jetstream::context::PublishError),
|
||||
}
|
||||
impl std::fmt::Display for HandleMessageError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for HandleMessageError{}
|
||||
|
||||
fn from_slice<'a,T:serde::de::Deserialize<'a>>(slice:&'a [u8])->Result<T,HandleMessageError>{
|
||||
serde_json::from_slice(slice).map_err(HandleMessageError::Json)
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
processor:&process::Processor,
|
||||
jetstream:&async_nats::jetstream::Context,
|
||||
message:async_nats::jetstream::Message,
|
||||
)->Result<(),HandleMessageError>{
|
||||
match message.subject.as_str(){
|
||||
SUBJECT_MAPFIX_RELEASE=>{
|
||||
let request:nats_types::ReleaseMapfixRequest=from_slice(&message.payload)?;
|
||||
processor.handle_mapfix_release(request).await.map_err(HandleMessageError::Process)?;
|
||||
message.ack().await.map_err(HandleMessageError::Ack)?;
|
||||
},
|
||||
SUBJECT_SUBMISSION_BATCHRELEASE=>{
|
||||
// split batch into individual seed messages
|
||||
let batch:nats_types::ReleaseSubmissionsBatchRequest=from_slice(&message.payload)?;
|
||||
println!("[combobulator] Splitting batch release (operation {}, {} submissions)",
|
||||
batch.OperationID,batch.Submissions.len());
|
||||
for submission in batch.Submissions{
|
||||
let seed=nats_types::SeedCombobulatorRequest{AssetID:submission.UploadedAssetID};
|
||||
let payload=serde_json::to_vec(&seed).map_err(HandleMessageError::Json)?;
|
||||
jetstream.publish(SUBJECT_SEED,payload.into())
|
||||
.await.map_err(HandleMessageError::Publish)?;
|
||||
println!("[combobulator] Queued seed for asset {}",seed.AssetID);
|
||||
}
|
||||
message.ack().await.map_err(HandleMessageError::Ack)?;
|
||||
},
|
||||
SUBJECT_SEED=>{
|
||||
let request:nats_types::SeedCombobulatorRequest=from_slice(&message.payload)?;
|
||||
processor.handle_seed(request).await.map_err(HandleMessageError::Process)?;
|
||||
message.ack().await.map_err(HandleMessageError::Ack)?;
|
||||
},
|
||||
other=>return Err(HandleMessageError::UnknownSubject(other.to_owned())),
|
||||
}
|
||||
|
||||
println!("[combobulator] Message processed and acked");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main()->Result<(),StartupError>{
|
||||
// roblox cookie api for downloading assets
|
||||
let cookie=std::env::var("RBXCOOKIE").expect("RBXCOOKIE env required");
|
||||
let cookie_context=rbx_asset::cookie::Context::new(rbx_asset::cookie::Cookie::new(cookie));
|
||||
|
||||
// s3
|
||||
let s3_bucket=std::env::var("S3_BUCKET").expect("S3_BUCKET env required");
|
||||
let s3_config=aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
||||
let s3_client=aws_sdk_s3::Client::new(&s3_config);
|
||||
let s3_cache=s3::S3Cache::new(s3_client,s3_bucket);
|
||||
|
||||
let processor=process::Processor{
|
||||
cookie_context,
|
||||
s3:s3_cache,
|
||||
};
|
||||
|
||||
// nats
|
||||
let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required");
|
||||
|
||||
const STREAM_NAME:&str="maptest";
|
||||
const DURABLE_NAME:&str="combobulator";
|
||||
|
||||
let filter_subjects=vec![
|
||||
SUBJECT_MAPFIX_RELEASE.to_owned(),
|
||||
SUBJECT_SUBMISSION_BATCHRELEASE.to_owned(),
|
||||
SUBJECT_SEED.to_owned(),
|
||||
];
|
||||
|
||||
let nats_config=async_nats::jetstream::consumer::pull::Config{
|
||||
name:Some(DURABLE_NAME.to_owned()),
|
||||
durable_name:Some(DURABLE_NAME.to_owned()),
|
||||
filter_subjects:filter_subjects.clone(),
|
||||
ack_wait:std::time::Duration::from_secs(900), // 15 minutes for processing
|
||||
max_deliver:5, // retry up to 5 times
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let nasty=async_nats::connect(nats_host).await.map_err(StartupError::NatsConnect)?;
|
||||
let jetstream=async_nats::jetstream::new(nasty);
|
||||
let stream=jetstream.get_stream(STREAM_NAME).await.map_err(StartupError::NatsGetStream)?;
|
||||
let consumer=stream.get_or_create_consumer(DURABLE_NAME,nats_config.clone()).await.map_err(StartupError::NatsConsumer)?;
|
||||
|
||||
// update consumer config if filter subjects changed
|
||||
if consumer.cached_info().config.filter_subjects!=filter_subjects{
|
||||
stream.update_consumer(nats_config).await.map_err(StartupError::NatsConsumerUpdate)?;
|
||||
}
|
||||
|
||||
let mut messages=consumer.messages().await.map_err(StartupError::NatsStream)?;
|
||||
|
||||
// SIGTERM graceful shutdown
|
||||
let mut sig_term=tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("Failed to create SIGTERM signal listener");
|
||||
|
||||
println!("[combobulator] Started, waiting for messages...");
|
||||
|
||||
// sequential processing loop - one message at a time
|
||||
let main_loop=async{
|
||||
while let Some(message_result)=messages.next().await{
|
||||
match message_result{
|
||||
Ok(message)=>{
|
||||
match handle_message(&processor,&jetstream,message).await{
|
||||
Ok(())=>println!("[combobulator] Success"),
|
||||
Err(e)=>println!("[combobulator] Error: {e}"),
|
||||
}
|
||||
},
|
||||
Err(e)=>println!("[combobulator] Message stream error: {e}"),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select!{
|
||||
_=sig_term.recv()=>{
|
||||
println!("[combobulator] Received SIGTERM, shutting down");
|
||||
},
|
||||
_=main_loop=>{
|
||||
println!("[combobulator] Message stream ended");
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#[expect(nonstandard_style,dead_code)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseMapfixRequest{
|
||||
pub MapfixID:u64,
|
||||
pub ModelID:u64,
|
||||
pub ModelVersion:u64,
|
||||
pub TargetAssetID:u64,
|
||||
}
|
||||
|
||||
#[expect(nonstandard_style,dead_code)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseSubmissionRequest{
|
||||
pub SubmissionID:u64,
|
||||
pub ReleaseDate:i64,
|
||||
pub ModelID:u64,
|
||||
pub ModelVersion:u64,
|
||||
pub UploadedAssetID:u64,
|
||||
pub DisplayName:String,
|
||||
pub Creator:String,
|
||||
pub GameID:u32,
|
||||
pub Submitter:u64,
|
||||
}
|
||||
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReleaseSubmissionsBatchRequest{
|
||||
pub Submissions:Vec<ReleaseSubmissionRequest>,
|
||||
pub OperationID:u32,
|
||||
}
|
||||
|
||||
#[expect(nonstandard_style)]
|
||||
#[derive(serde::Deserialize,serde::Serialize)]
|
||||
pub struct SeedCombobulatorRequest{
|
||||
pub AssetID:u64,
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::nats_types::ReleaseMapfixRequest;
|
||||
use crate::s3::S3Cache;
|
||||
|
||||
use futures_util::stream::iter as stream_iter;
|
||||
use futures_util::{StreamExt,TryStreamExt};
|
||||
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
|
||||
|
||||
const CONCURRENT_REQUESTS:usize=16;
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ConvertError{
|
||||
IO(std::io::Error),
|
||||
SNFMap(strafesnet_snf::map::Error),
|
||||
RobloxLoadMesh(super::loader::MeshError),
|
||||
RobloxLoadTexture(super::loader::TextureError),
|
||||
}
|
||||
impl std::fmt::Display for ConvertError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ConvertError{}
|
||||
|
||||
pub fn convert_to_snf(
|
||||
dom:rbx_dom_weak::WeakDom,
|
||||
mut mesh_loader:crate::loader::MeshLoader,
|
||||
mut texture_loader:crate::loader::TextureLoader,
|
||||
)->Result<Vec<u8>,ConvertError>{
|
||||
const FAILURE_MODE:LoadFailureMode=LoadFailureMode::DefaultToNone;
|
||||
|
||||
// run scripts
|
||||
let model=strafesnet_rbx_loader::Model::new(dom);
|
||||
|
||||
let mut place=strafesnet_rbx_loader::Place::from(model);
|
||||
// TODO: script errors report for burn down chart
|
||||
let _script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
|
||||
|
||||
// convert
|
||||
let mut texture_deferred_loader=strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader::new();
|
||||
let mut mesh_deferred_loader=strafesnet_deferred_loader::deferred_loader::MeshDeferredLoader::new();
|
||||
|
||||
let map_step1=strafesnet_rbx_loader::rbx::convert(
|
||||
place.as_ref(),
|
||||
&mut texture_deferred_loader,
|
||||
&mut mesh_deferred_loader,
|
||||
);
|
||||
|
||||
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadMesh)?;
|
||||
|
||||
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
|
||||
|
||||
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadTexture)?;
|
||||
|
||||
// TODO: conversion error report for burn down chart
|
||||
let (map,_convert_errors)=map_step2.add_render_configs_and_textures(render_configs);
|
||||
|
||||
let mut snf_buf=Vec::new();
|
||||
strafesnet_snf::map::write_map(Cursor::new(&mut snf_buf),map).map_err(ConvertError::SNFMap)?;
|
||||
|
||||
Ok(snf_buf)
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error{
|
||||
ArchivedModel,
|
||||
LoadDom(map_tool::roblox::LoadDomError),
|
||||
DownloadAsset(map_tool::roblox::DownloadAssetError),
|
||||
ConvertSnf(ConvertError),
|
||||
S3Get(crate::s3::GetError),
|
||||
S3Put(crate::s3::PutError),
|
||||
}
|
||||
impl std::fmt::Display for Error{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for Error{}
|
||||
|
||||
pub struct Processor{
|
||||
pub cookie_context:rbx_asset::cookie::Context,
|
||||
pub s3:S3Cache,
|
||||
}
|
||||
|
||||
impl Processor{
|
||||
/// Download an asset, returning None if the asset is archived.
|
||||
async fn download_asset(&self,asset_id:u64)->Result<Option<Vec<u8>>,Error>{
|
||||
match map_tool::roblox::download_asset(&self.cookie_context,asset_id).await{
|
||||
Ok(data)=>Ok(Some(data)),
|
||||
Err(e)=>{
|
||||
let s=format!("{e:?}");
|
||||
if s.contains("Requested asset is archived"){
|
||||
println!("[combobulator] Asset {asset_id} is archived, skipping");
|
||||
Ok(None)
|
||||
}else if s.contains("User is not authorized to access Asset"){
|
||||
println!("[combobulator] User is not authorized to access Asset {asset_id}, skipping");
|
||||
Ok(None)
|
||||
}else if s.contains("Asset is not approved for the requester"){
|
||||
println!("[combobulator] Asset {asset_id} is not approved for the requester, skipping");
|
||||
Ok(None)
|
||||
}else if s.contains("Request asset was not found"){
|
||||
println!("[combobulator] Asset {asset_id} was not found, skipping");
|
||||
Ok(None)
|
||||
}else{
|
||||
Err(Error::DownloadAsset(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a single model: extract assets, cache to S3, build SNF.
|
||||
async fn process_model(&self,asset_id:u64)->Result<(),Error>{
|
||||
println!("[combobulator] Downloading model {asset_id}");
|
||||
let rbxl_bytes=self.download_asset(asset_id).await?
|
||||
.ok_or(Error::ArchivedModel)?;
|
||||
|
||||
// decode dom
|
||||
let dom=map_tool::roblox::load_dom(&rbxl_bytes)
|
||||
.map_err(Error::LoadDom)?;
|
||||
|
||||
// extract unique assets from the file
|
||||
let assets=map_tool::roblox::get_unique_assets(&dom);
|
||||
|
||||
// place textures into 'loader'
|
||||
let texture_loader=crate::loader::TextureLoader::new();
|
||||
// process textures: download, cache, convert to DDS
|
||||
let texture_loader=stream_iter(assets.textures).map(async|id|{
|
||||
let asset_id=id.0;
|
||||
let dds_key=S3Cache::texture_dds_key(asset_id);
|
||||
|
||||
// fetch cached DDS
|
||||
let dds=if let Some(dds)=self.s3.get(&dds_key).await.map_err(Error::S3Get)?{
|
||||
dds
|
||||
}else{
|
||||
// check raw cache, download if missing
|
||||
let raw_key=S3Cache::texture_raw_key(asset_id);
|
||||
let dds_result=if let Some(data)=self.s3.get(&raw_key).await.map_err(Error::S3Get)?{
|
||||
map_tool::roblox::convert_texture_to_dds(&data)
|
||||
}else{
|
||||
println!("[combobulator] Downloading texture {asset_id}");
|
||||
let Some(data)=self.download_asset(asset_id).await? else{
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// decode while we have ownership
|
||||
let dds_result=map_tool::roblox::convert_texture_to_dds(&data);
|
||||
|
||||
self.s3.put(&raw_key,data).await.map_err(Error::S3Put)?;
|
||||
dds_result
|
||||
};
|
||||
|
||||
// handle error after cacheing data
|
||||
let dds=match dds_result{
|
||||
Ok(dds)=>dds,
|
||||
Err(e)=>{
|
||||
println!("[combobulator] Texture {asset_id} convert error: {e}");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
self.s3.put(&dds_key,dds.clone()).await.map_err(Error::S3Put)?;
|
||||
|
||||
dds
|
||||
};
|
||||
println!("[combobulator] Texture {asset_id} processed");
|
||||
|
||||
Ok(Some((id,dds)))
|
||||
})
|
||||
.buffer_unordered(CONCURRENT_REQUESTS)
|
||||
.try_fold(texture_loader,async|mut texture_loader,maybe_loaded_texture|{
|
||||
if let Some((id,dds))=maybe_loaded_texture{
|
||||
texture_loader.insert(id,dds);
|
||||
}
|
||||
Ok(texture_loader)
|
||||
}).await?;
|
||||
|
||||
let mesh_loader=crate::loader::MeshLoader::new();
|
||||
// process meshes
|
||||
let mesh_loader=stream_iter(assets.meshes).map(async|id|{
|
||||
let asset_id=id.0;
|
||||
let mesh_key=S3Cache::mesh_key(asset_id);
|
||||
|
||||
let mesh_result=if let Some(data)=self.s3.get(&mesh_key).await.map_err(Error::S3Get)?{
|
||||
strafesnet_rbx_loader::mesh::convert(&data)
|
||||
}else{
|
||||
println!("[combobulator] Downloading mesh {asset_id}");
|
||||
let Some(data)=self.download_asset(asset_id).await? else{
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// decode while we have ownership
|
||||
let mesh_result=strafesnet_rbx_loader::mesh::convert(&data);
|
||||
|
||||
self.s3.put(&mesh_key,data.clone()).await.map_err(Error::S3Put)?;
|
||||
mesh_result
|
||||
};
|
||||
println!("[combobulator] Mesh {asset_id} processed");
|
||||
|
||||
// handle error after cacheing data
|
||||
match mesh_result{
|
||||
Ok(mesh)=>Ok(Some((id,mesh))),
|
||||
Err(e)=>{
|
||||
println!("[combobulator] Mesh {asset_id} convert error: {e}");
|
||||
Ok(None)
|
||||
},
|
||||
}
|
||||
})
|
||||
.buffer_unordered(CONCURRENT_REQUESTS)
|
||||
.try_fold(mesh_loader,async|mut mesh_loader,maybe_loaded_mesh|{
|
||||
if let Some((id,mesh))=maybe_loaded_mesh{
|
||||
mesh_loader.insert_mesh(id,mesh);
|
||||
}
|
||||
Ok(mesh_loader)
|
||||
}).await?;
|
||||
|
||||
// process unions
|
||||
let mesh_loader=stream_iter(assets.unions).map(async|id|{
|
||||
let asset_id=id.0;
|
||||
let union_key=S3Cache::union_key(asset_id);
|
||||
|
||||
let union_result=if let Some(data)=self.s3.get(&union_key).await.map_err(Error::S3Get)?{
|
||||
rbx_binary::from_reader(data.as_slice())
|
||||
}else{
|
||||
println!("[combobulator] Downloading union {asset_id}");
|
||||
let Some(data)=self.download_asset(asset_id).await? else{
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// decode the data while we have ownership
|
||||
let union_result=rbx_binary::from_reader(data.as_slice());
|
||||
|
||||
self.s3.put(&union_key,data).await.map_err(Error::S3Put)?;
|
||||
union_result
|
||||
};
|
||||
println!("[combobulator] Union {asset_id} processed");
|
||||
|
||||
// handle error after cacheing data
|
||||
match union_result{
|
||||
Ok(union)=>Ok(Some((id,union))),
|
||||
Err(e)=>{
|
||||
println!("[combobulator] Union {asset_id} convert error: {e}");
|
||||
Ok(None)
|
||||
},
|
||||
}
|
||||
})
|
||||
.buffer_unordered(CONCURRENT_REQUESTS)
|
||||
.try_fold(mesh_loader,async|mut mesh_loader,maybe_loaded_union|{
|
||||
if let Some((id,union))=maybe_loaded_union{
|
||||
mesh_loader.insert_union(id,union);
|
||||
}
|
||||
Ok(mesh_loader)
|
||||
}).await?;
|
||||
|
||||
// convert to SNF and upload
|
||||
println!("[combobulator] Converting to SNF");
|
||||
let snf=convert_to_snf(dom,mesh_loader,texture_loader)
|
||||
.map_err(Error::ConvertSnf)?;
|
||||
let snf_key=S3Cache::snf_key(asset_id);
|
||||
self.s3.put(&snf_key,snf).await.map_err(Error::S3Put)?;
|
||||
println!("[combobulator] SNF uploaded to {snf_key}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a mapfix release message.
|
||||
pub async fn handle_mapfix_release(&self,request:ReleaseMapfixRequest)->Result<(),Error>{
|
||||
println!("[combobulator] Processing mapfix {} (asset {})",
|
||||
request.MapfixID,request.TargetAssetID);
|
||||
self.process_model(request.TargetAssetID).await
|
||||
}
|
||||
|
||||
/// Handle a seed request (reprocess an existing map).
|
||||
pub async fn handle_seed(&self,request:crate::nats_types::SeedCombobulatorRequest)->Result<(),Error>{
|
||||
println!("[combobulator] Seeding asset {}",request.AssetID);
|
||||
self.process_model(request.AssetID).await
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
use aws_sdk_s3::Client;
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum GetError{
|
||||
Get(aws_sdk_s3::error::SdkError<aws_sdk_s3::operation::get_object::GetObjectError>),
|
||||
Collect(aws_sdk_s3::primitives::ByteStreamError),
|
||||
}
|
||||
impl std::fmt::Display for GetError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for GetError{}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum PutError{
|
||||
Put(aws_sdk_s3::error::SdkError<aws_sdk_s3::operation::put_object::PutObjectError>),
|
||||
}
|
||||
impl std::fmt::Display for PutError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for PutError{}
|
||||
|
||||
pub struct S3Cache{
|
||||
client:Client,
|
||||
bucket:String,
|
||||
}
|
||||
|
||||
impl S3Cache{
|
||||
pub fn new(client:Client,bucket:String)->Self{
|
||||
Self{client,bucket}
|
||||
}
|
||||
|
||||
/// Try to get a cached object. Returns None if the key doesn't exist.
|
||||
pub async fn get(&self,key:&str)->Result<Option<Vec<u8>>,GetError>{
|
||||
match self.client.get_object()
|
||||
.bucket(&self.bucket)
|
||||
.key(key)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(output)=>{
|
||||
let bytes=output.body.collect().await.map_err(GetError::Collect)?;
|
||||
Ok(Some(bytes.to_vec()))
|
||||
},
|
||||
Err(e)=>{
|
||||
// check if it's a NoSuchKey error
|
||||
if let aws_sdk_s3::error::SdkError::ServiceError(ref service_err)=e{
|
||||
if service_err.err().is_no_such_key(){
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Err(GetError::Get(e))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Put an object into S3.
|
||||
pub async fn put(&self,key:&str,data:Vec<u8>)->Result<(),PutError>{
|
||||
self.client.put_object()
|
||||
.bucket(&self.bucket)
|
||||
.key(key)
|
||||
.body(ByteStream::from(data))
|
||||
.send()
|
||||
.await
|
||||
.map_err(PutError::Put)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// S3 key helpers
|
||||
|
||||
pub fn texture_raw_key(asset_id:u64)->String{
|
||||
format!("assets/textures/{asset_id}.raw")
|
||||
}
|
||||
|
||||
pub fn texture_dds_key(asset_id:u64)->String{
|
||||
format!("assets/textures/{asset_id}.dds")
|
||||
}
|
||||
|
||||
pub fn mesh_key(asset_id:u64)->String{
|
||||
format!("assets/meshes/{asset_id}")
|
||||
}
|
||||
|
||||
pub fn union_key(asset_id:u64)->String{
|
||||
format!("assets/unions/{asset_id}")
|
||||
}
|
||||
|
||||
pub fn snf_key(model_id:u64)->String{
|
||||
format!("maps/{model_id}.snfm")
|
||||
}
|
||||
}
|
||||
40
docs/docs.go
40
docs/docs.go
@@ -115,46 +115,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/map/{id}/snfm": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Redirects to a signed download URL for a map's SNFM file",
|
||||
"tags": [
|
||||
"maps"
|
||||
],
|
||||
"summary": "Download SNFM file",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Map ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"307": {
|
||||
"description": "Redirect to signed S3 URL"
|
||||
},
|
||||
"404": {
|
||||
"description": "Map not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "General error response",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
||||
@@ -108,46 +108,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/map/{id}/snfm": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Redirects to a signed download URL for a map's SNFM file",
|
||||
"tags": [
|
||||
"maps"
|
||||
],
|
||||
"summary": "Download SNFM file",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Map ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"307": {
|
||||
"description": "Redirect to signed S3 URL"
|
||||
},
|
||||
"404": {
|
||||
"description": "Map not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "General error response",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
||||
@@ -133,31 +133,6 @@ paths:
|
||||
summary: Get map by ID
|
||||
tags:
|
||||
- maps
|
||||
/map/{id}/snfm:
|
||||
get:
|
||||
description: Redirects to a signed download URL for a map's SNFM file
|
||||
parameters:
|
||||
- description: Map ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
responses:
|
||||
"307":
|
||||
description: Redirect to signed S3 URL
|
||||
"404":
|
||||
description: Map not found
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
default:
|
||||
description: General error response
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Download SNFM file
|
||||
tags:
|
||||
- maps
|
||||
securityDefinitions:
|
||||
ApiKeyAuth:
|
||||
in: header
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
|
||||
//go:generate go run github.com/swaggo/swag/cmd/swag@latest init -g ./cmd/maps-service/service.go
|
||||
//go:generate swag init -g ./cmd/maps-service/service.go
|
||||
//go:generate go run github.com/ogen-go/ogen/cmd/ogen@latest --target pkg/api --clean openapi.yaml
|
||||
|
||||
27
go.mod
27
go.mod
@@ -6,7 +6,7 @@ toolchain go1.24.5
|
||||
|
||||
require (
|
||||
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301211036-f2db3cb46e8c
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20250815013325-1c84f73bdcb1
|
||||
git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9
|
||||
github.com/dchest/siphash v1.2.3
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
@@ -20,9 +20,9 @@ require (
|
||||
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.40.0
|
||||
go.opentelemetry.io/otel/metric v1.40.0
|
||||
go.opentelemetry.io/otel/trace v1.40.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
|
||||
@@ -32,25 +32,6 @@ require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
|
||||
github.com/aws/smithy-go v1.24.1 // 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
|
||||
|
||||
86
go.sum
86
go.sum
@@ -2,12 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed h1:eGWIQx2AOrSsLC2dieuSs8MCliRE60tvpZnmxsTBtKc=
|
||||
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed/go.mod h1:KJal0K++M6HEzSry6JJ2iDPZtOQn5zSstNlDbU3X4Jg=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20251228204118-c20dbb42afec h1:JSar9If1kzb02+Erp+zmSqHKWPPP2NqMQVK15pRmkLE=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20251228204118-c20dbb42afec/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301210537-0bea64387f6d h1:I73hWqmIcsSH90VHjwsg50v6emQkM0IAA04vb4wktBA=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301210537-0bea64387f6d/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301211036-f2db3cb46e8c h1:sI50ymozoI+HFbxg1AOdCeWF6bJgpeP6OrnCvyjuQ9U=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301211036-f2db3cb46e8c/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20250815013325-1c84f73bdcb1 h1:imXibfeYcae6og0TTDUFRQ3CQtstGjIoLbCn+pezD2o=
|
||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20250815013325-1c84f73bdcb1/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
|
||||
git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9 h1:7lU6jyR7S7Rhh1dnUp7GyIRHUTBXZagw8F4n4hOyxLw=
|
||||
git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9/go.mod h1:uyYerSieEt4v0MJCdPLppG0LtJ4Yj035vuTetWGsxjY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
@@ -18,44 +14,6 @@ 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/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
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=
|
||||
@@ -89,6 +47,8 @@ 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=
|
||||
@@ -97,6 +57,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
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=
|
||||
@@ -111,6 +73,8 @@ github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
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=
|
||||
@@ -188,6 +152,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
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=
|
||||
@@ -207,8 +173,11 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
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=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -223,6 +192,8 @@ github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDm
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
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=
|
||||
@@ -237,6 +208,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
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=
|
||||
@@ -279,21 +252,17 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
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 v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
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/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
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/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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=
|
||||
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=
|
||||
@@ -302,15 +271,21 @@ golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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=
|
||||
@@ -326,6 +301,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
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=
|
||||
@@ -335,6 +312,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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=
|
||||
@@ -350,8 +329,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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=
|
||||
@@ -362,6 +344,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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=
|
||||
@@ -371,6 +355,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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=
|
||||
|
||||
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 "$@"
|
||||
331
openapi.yaml
331
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
|
||||
@@ -184,29 +186,6 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/maps/{MapID}/combobulate:
|
||||
post:
|
||||
summary: Queue a map for combobulator processing
|
||||
operationId: combobulateMap
|
||||
tags:
|
||||
- Maps
|
||||
parameters:
|
||||
- name: MapID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
responses:
|
||||
"204":
|
||||
description: Successful response
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/maps/{MapID}/download:
|
||||
get:
|
||||
summary: Download the map asset
|
||||
@@ -235,21 +214,6 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/maps-admin/seed-combobulator:
|
||||
post:
|
||||
summary: Queue all maps for combobulator processing
|
||||
operationId: seedCombobulator
|
||||
tags:
|
||||
- Maps
|
||||
responses:
|
||||
"204":
|
||||
description: Successful response
|
||||
default:
|
||||
description: General Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/mapfixes:
|
||||
get:
|
||||
summary: Get list of mapfixes
|
||||
@@ -998,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
|
||||
@@ -1262,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
|
||||
@@ -1821,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:
|
||||
@@ -2406,6 +2606,61 @@ components:
|
||||
- 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
|
||||
|
||||
@@ -4,7 +4,6 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
ht "github.com/ogen-go/ogen/http"
|
||||
"github.com/ogen-go/ogen/middleware"
|
||||
@@ -83,8 +82,18 @@ func (o otelOptionFunc) applyServer(c *serverConfig) {
|
||||
|
||||
func newServerConfig(opts ...ServerOption) serverConfig {
|
||||
cfg := serverConfig{
|
||||
NotFound: http.NotFound,
|
||||
MethodNotAllowed: nil,
|
||||
NotFound: http.NotFound,
|
||||
MethodNotAllowed: func(w http.ResponseWriter, r *http.Request, allowed string) {
|
||||
status := http.StatusMethodNotAllowed
|
||||
if r.Method == "OPTIONS" {
|
||||
w.Header().Set("Access-Control-Allow-Methods", allowed)
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
status = http.StatusNoContent
|
||||
} else {
|
||||
w.Header().Set("Allow", allowed)
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
},
|
||||
ErrorHandler: ogenerrors.DefaultErrorHandler,
|
||||
Middleware: nil,
|
||||
MaxMultipartMemory: 32 << 20, // 32 MB
|
||||
@@ -107,44 +116,8 @@ func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) {
|
||||
s.cfg.NotFound(w, r)
|
||||
}
|
||||
|
||||
type notAllowedParams struct {
|
||||
allowedMethods string
|
||||
allowedHeaders map[string]string
|
||||
acceptPost string
|
||||
acceptPatch string
|
||||
}
|
||||
|
||||
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, params notAllowedParams) {
|
||||
h := w.Header()
|
||||
isOptions := r.Method == "OPTIONS"
|
||||
if isOptions {
|
||||
h.Set("Access-Control-Allow-Methods", params.allowedMethods)
|
||||
if params.allowedHeaders != nil {
|
||||
m := r.Header.Get("Access-Control-Request-Method")
|
||||
if m != "" {
|
||||
allowedHeaders, ok := params.allowedHeaders[strings.ToUpper(m)]
|
||||
if ok {
|
||||
h.Set("Access-Control-Allow-Headers", allowedHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
if params.acceptPost != "" {
|
||||
h.Set("Accept-Post", params.acceptPost)
|
||||
}
|
||||
if params.acceptPatch != "" {
|
||||
h.Set("Accept-Patch", params.acceptPatch)
|
||||
}
|
||||
}
|
||||
if s.cfg.MethodNotAllowed != nil {
|
||||
s.cfg.MethodNotAllowed(w, r, params.allowedMethods)
|
||||
return
|
||||
}
|
||||
status := http.StatusNoContent
|
||||
if !isOptions {
|
||||
h.Set("Allow", params.allowedMethods)
|
||||
status = http.StatusMethodNotAllowed
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, allowed string) {
|
||||
s.cfg.MethodNotAllowed(w, r, allowed)
|
||||
}
|
||||
|
||||
func (cfg serverConfig) baseServer() (s baseServer, err error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,221 @@ import (
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
)
|
||||
|
||||
// Encode implements json.Marshaler.
|
||||
func (s *AOREvent) Encode(e *jx.Encoder) {
|
||||
e.ObjStart()
|
||||
s.encodeFields(e)
|
||||
e.ObjEnd()
|
||||
}
|
||||
|
||||
// encodeFields encodes fields.
|
||||
func (s *AOREvent) encodeFields(e *jx.Encoder) {
|
||||
{
|
||||
e.FieldStart("ID")
|
||||
e.Int64(s.ID)
|
||||
}
|
||||
{
|
||||
e.FieldStart("StartDate")
|
||||
e.Int64(s.StartDate)
|
||||
}
|
||||
{
|
||||
e.FieldStart("FreezeDate")
|
||||
e.Int64(s.FreezeDate)
|
||||
}
|
||||
{
|
||||
e.FieldStart("SelectionDate")
|
||||
e.Int64(s.SelectionDate)
|
||||
}
|
||||
{
|
||||
e.FieldStart("DecisionDate")
|
||||
e.Int64(s.DecisionDate)
|
||||
}
|
||||
{
|
||||
e.FieldStart("Status")
|
||||
e.Int32(s.Status)
|
||||
}
|
||||
{
|
||||
e.FieldStart("CreatedAt")
|
||||
e.Int64(s.CreatedAt)
|
||||
}
|
||||
{
|
||||
e.FieldStart("UpdatedAt")
|
||||
e.Int64(s.UpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
var jsonFieldsNameOfAOREvent = [8]string{
|
||||
0: "ID",
|
||||
1: "StartDate",
|
||||
2: "FreezeDate",
|
||||
3: "SelectionDate",
|
||||
4: "DecisionDate",
|
||||
5: "Status",
|
||||
6: "CreatedAt",
|
||||
7: "UpdatedAt",
|
||||
}
|
||||
|
||||
// Decode decodes AOREvent from json.
|
||||
func (s *AOREvent) Decode(d *jx.Decoder) error {
|
||||
if s == nil {
|
||||
return errors.New("invalid: unable to decode AOREvent to nil")
|
||||
}
|
||||
var requiredBitSet [1]uint8
|
||||
|
||||
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
|
||||
switch string(k) {
|
||||
case "ID":
|
||||
requiredBitSet[0] |= 1 << 0
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.ID = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"ID\"")
|
||||
}
|
||||
case "StartDate":
|
||||
requiredBitSet[0] |= 1 << 1
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.StartDate = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"StartDate\"")
|
||||
}
|
||||
case "FreezeDate":
|
||||
requiredBitSet[0] |= 1 << 2
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.FreezeDate = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"FreezeDate\"")
|
||||
}
|
||||
case "SelectionDate":
|
||||
requiredBitSet[0] |= 1 << 3
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.SelectionDate = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"SelectionDate\"")
|
||||
}
|
||||
case "DecisionDate":
|
||||
requiredBitSet[0] |= 1 << 4
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.DecisionDate = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"DecisionDate\"")
|
||||
}
|
||||
case "Status":
|
||||
requiredBitSet[0] |= 1 << 5
|
||||
if err := func() error {
|
||||
v, err := d.Int32()
|
||||
s.Status = int32(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"Status\"")
|
||||
}
|
||||
case "CreatedAt":
|
||||
requiredBitSet[0] |= 1 << 6
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.CreatedAt = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"CreatedAt\"")
|
||||
}
|
||||
case "UpdatedAt":
|
||||
requiredBitSet[0] |= 1 << 7
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.UpdatedAt = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"UpdatedAt\"")
|
||||
}
|
||||
default:
|
||||
return d.Skip()
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "decode AOREvent")
|
||||
}
|
||||
// Validate required fields.
|
||||
var failures []validate.FieldError
|
||||
for i, mask := range [1]uint8{
|
||||
0b11111111,
|
||||
} {
|
||||
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
|
||||
// Mask only required fields and check equality to mask using XOR.
|
||||
//
|
||||
// If XOR result is not zero, result is not equal to expected, so some fields are missed.
|
||||
// Bits of fields which would be set are actually bits of missed fields.
|
||||
missed := bits.OnesCount8(result)
|
||||
for bitN := 0; bitN < missed; bitN++ {
|
||||
bitIdx := bits.TrailingZeros8(result)
|
||||
fieldIdx := i*8 + bitIdx
|
||||
var name string
|
||||
if fieldIdx < len(jsonFieldsNameOfAOREvent) {
|
||||
name = jsonFieldsNameOfAOREvent[fieldIdx]
|
||||
} else {
|
||||
name = strconv.Itoa(fieldIdx)
|
||||
}
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: name,
|
||||
Error: validate.ErrFieldRequired,
|
||||
})
|
||||
// Reset bit.
|
||||
result &^= 1 << bitIdx
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements stdjson.Marshaler.
|
||||
func (s *AOREvent) MarshalJSON() ([]byte, error) {
|
||||
e := jx.Encoder{}
|
||||
s.Encode(&e)
|
||||
return e.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements stdjson.Unmarshaler.
|
||||
func (s *AOREvent) UnmarshalJSON(data []byte) error {
|
||||
d := jx.DecodeBytes(data)
|
||||
return s.Decode(d)
|
||||
}
|
||||
|
||||
// Encode implements json.Marshaler.
|
||||
func (s *AuditEvent) Encode(e *jx.Encoder) {
|
||||
e.ObjStart()
|
||||
@@ -4435,6 +4650,334 @@ func (s *Submission) UnmarshalJSON(data []byte) error {
|
||||
return s.Decode(d)
|
||||
}
|
||||
|
||||
// Encode implements json.Marshaler.
|
||||
func (s *SubmissionReview) Encode(e *jx.Encoder) {
|
||||
e.ObjStart()
|
||||
s.encodeFields(e)
|
||||
e.ObjEnd()
|
||||
}
|
||||
|
||||
// encodeFields encodes fields.
|
||||
func (s *SubmissionReview) encodeFields(e *jx.Encoder) {
|
||||
{
|
||||
e.FieldStart("ID")
|
||||
e.Int64(s.ID)
|
||||
}
|
||||
{
|
||||
e.FieldStart("SubmissionID")
|
||||
e.Int64(s.SubmissionID)
|
||||
}
|
||||
{
|
||||
e.FieldStart("ReviewerID")
|
||||
e.Int64(s.ReviewerID)
|
||||
}
|
||||
{
|
||||
e.FieldStart("Recommend")
|
||||
e.Bool(s.Recommend)
|
||||
}
|
||||
{
|
||||
e.FieldStart("Description")
|
||||
e.Str(s.Description)
|
||||
}
|
||||
{
|
||||
e.FieldStart("Outdated")
|
||||
e.Bool(s.Outdated)
|
||||
}
|
||||
{
|
||||
e.FieldStart("CreatedAt")
|
||||
e.Int64(s.CreatedAt)
|
||||
}
|
||||
{
|
||||
e.FieldStart("UpdatedAt")
|
||||
e.Int64(s.UpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
var jsonFieldsNameOfSubmissionReview = [8]string{
|
||||
0: "ID",
|
||||
1: "SubmissionID",
|
||||
2: "ReviewerID",
|
||||
3: "Recommend",
|
||||
4: "Description",
|
||||
5: "Outdated",
|
||||
6: "CreatedAt",
|
||||
7: "UpdatedAt",
|
||||
}
|
||||
|
||||
// Decode decodes SubmissionReview from json.
|
||||
func (s *SubmissionReview) Decode(d *jx.Decoder) error {
|
||||
if s == nil {
|
||||
return errors.New("invalid: unable to decode SubmissionReview to nil")
|
||||
}
|
||||
var requiredBitSet [1]uint8
|
||||
|
||||
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
|
||||
switch string(k) {
|
||||
case "ID":
|
||||
requiredBitSet[0] |= 1 << 0
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.ID = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"ID\"")
|
||||
}
|
||||
case "SubmissionID":
|
||||
requiredBitSet[0] |= 1 << 1
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.SubmissionID = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"SubmissionID\"")
|
||||
}
|
||||
case "ReviewerID":
|
||||
requiredBitSet[0] |= 1 << 2
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.ReviewerID = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"ReviewerID\"")
|
||||
}
|
||||
case "Recommend":
|
||||
requiredBitSet[0] |= 1 << 3
|
||||
if err := func() error {
|
||||
v, err := d.Bool()
|
||||
s.Recommend = bool(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"Recommend\"")
|
||||
}
|
||||
case "Description":
|
||||
requiredBitSet[0] |= 1 << 4
|
||||
if err := func() error {
|
||||
v, err := d.Str()
|
||||
s.Description = string(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"Description\"")
|
||||
}
|
||||
case "Outdated":
|
||||
requiredBitSet[0] |= 1 << 5
|
||||
if err := func() error {
|
||||
v, err := d.Bool()
|
||||
s.Outdated = bool(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"Outdated\"")
|
||||
}
|
||||
case "CreatedAt":
|
||||
requiredBitSet[0] |= 1 << 6
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.CreatedAt = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"CreatedAt\"")
|
||||
}
|
||||
case "UpdatedAt":
|
||||
requiredBitSet[0] |= 1 << 7
|
||||
if err := func() error {
|
||||
v, err := d.Int64()
|
||||
s.UpdatedAt = int64(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"UpdatedAt\"")
|
||||
}
|
||||
default:
|
||||
return d.Skip()
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "decode SubmissionReview")
|
||||
}
|
||||
// Validate required fields.
|
||||
var failures []validate.FieldError
|
||||
for i, mask := range [1]uint8{
|
||||
0b11111111,
|
||||
} {
|
||||
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
|
||||
// Mask only required fields and check equality to mask using XOR.
|
||||
//
|
||||
// If XOR result is not zero, result is not equal to expected, so some fields are missed.
|
||||
// Bits of fields which would be set are actually bits of missed fields.
|
||||
missed := bits.OnesCount8(result)
|
||||
for bitN := 0; bitN < missed; bitN++ {
|
||||
bitIdx := bits.TrailingZeros8(result)
|
||||
fieldIdx := i*8 + bitIdx
|
||||
var name string
|
||||
if fieldIdx < len(jsonFieldsNameOfSubmissionReview) {
|
||||
name = jsonFieldsNameOfSubmissionReview[fieldIdx]
|
||||
} else {
|
||||
name = strconv.Itoa(fieldIdx)
|
||||
}
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: name,
|
||||
Error: validate.ErrFieldRequired,
|
||||
})
|
||||
// Reset bit.
|
||||
result &^= 1 << bitIdx
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements stdjson.Marshaler.
|
||||
func (s *SubmissionReview) MarshalJSON() ([]byte, error) {
|
||||
e := jx.Encoder{}
|
||||
s.Encode(&e)
|
||||
return e.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements stdjson.Unmarshaler.
|
||||
func (s *SubmissionReview) UnmarshalJSON(data []byte) error {
|
||||
d := jx.DecodeBytes(data)
|
||||
return s.Decode(d)
|
||||
}
|
||||
|
||||
// Encode implements json.Marshaler.
|
||||
func (s *SubmissionReviewCreate) Encode(e *jx.Encoder) {
|
||||
e.ObjStart()
|
||||
s.encodeFields(e)
|
||||
e.ObjEnd()
|
||||
}
|
||||
|
||||
// encodeFields encodes fields.
|
||||
func (s *SubmissionReviewCreate) encodeFields(e *jx.Encoder) {
|
||||
{
|
||||
e.FieldStart("Recommend")
|
||||
e.Bool(s.Recommend)
|
||||
}
|
||||
{
|
||||
e.FieldStart("Description")
|
||||
e.Str(s.Description)
|
||||
}
|
||||
}
|
||||
|
||||
var jsonFieldsNameOfSubmissionReviewCreate = [2]string{
|
||||
0: "Recommend",
|
||||
1: "Description",
|
||||
}
|
||||
|
||||
// Decode decodes SubmissionReviewCreate from json.
|
||||
func (s *SubmissionReviewCreate) Decode(d *jx.Decoder) error {
|
||||
if s == nil {
|
||||
return errors.New("invalid: unable to decode SubmissionReviewCreate to nil")
|
||||
}
|
||||
var requiredBitSet [1]uint8
|
||||
|
||||
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
|
||||
switch string(k) {
|
||||
case "Recommend":
|
||||
requiredBitSet[0] |= 1 << 0
|
||||
if err := func() error {
|
||||
v, err := d.Bool()
|
||||
s.Recommend = bool(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"Recommend\"")
|
||||
}
|
||||
case "Description":
|
||||
requiredBitSet[0] |= 1 << 1
|
||||
if err := func() error {
|
||||
v, err := d.Str()
|
||||
s.Description = string(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return errors.Wrap(err, "decode field \"Description\"")
|
||||
}
|
||||
default:
|
||||
return d.Skip()
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "decode SubmissionReviewCreate")
|
||||
}
|
||||
// Validate required fields.
|
||||
var failures []validate.FieldError
|
||||
for i, mask := range [1]uint8{
|
||||
0b00000011,
|
||||
} {
|
||||
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
|
||||
// Mask only required fields and check equality to mask using XOR.
|
||||
//
|
||||
// If XOR result is not zero, result is not equal to expected, so some fields are missed.
|
||||
// Bits of fields which would be set are actually bits of missed fields.
|
||||
missed := bits.OnesCount8(result)
|
||||
for bitN := 0; bitN < missed; bitN++ {
|
||||
bitIdx := bits.TrailingZeros8(result)
|
||||
fieldIdx := i*8 + bitIdx
|
||||
var name string
|
||||
if fieldIdx < len(jsonFieldsNameOfSubmissionReviewCreate) {
|
||||
name = jsonFieldsNameOfSubmissionReviewCreate[fieldIdx]
|
||||
} else {
|
||||
name = strconv.Itoa(fieldIdx)
|
||||
}
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: name,
|
||||
Error: validate.ErrFieldRequired,
|
||||
})
|
||||
// Reset bit.
|
||||
result &^= 1 << bitIdx
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements stdjson.Marshaler.
|
||||
func (s *SubmissionReviewCreate) MarshalJSON() ([]byte, error) {
|
||||
e := jx.Encoder{}
|
||||
s.Encode(&e)
|
||||
return e.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements stdjson.Unmarshaler.
|
||||
func (s *SubmissionReviewCreate) UnmarshalJSON(data []byte) error {
|
||||
d := jx.DecodeBytes(data)
|
||||
return s.Decode(d)
|
||||
}
|
||||
|
||||
// Encode implements json.Marshaler.
|
||||
func (s *SubmissionTriggerCreate) Encode(e *jx.Encoder) {
|
||||
e.ObjStart()
|
||||
|
||||
@@ -33,7 +33,6 @@ const (
|
||||
BatchAssetThumbnailsOperation OperationName = "BatchAssetThumbnails"
|
||||
BatchUserThumbnailsOperation OperationName = "BatchUserThumbnails"
|
||||
BatchUsernamesOperation OperationName = "BatchUsernames"
|
||||
CombobulateMapOperation OperationName = "CombobulateMap"
|
||||
CreateMapfixOperation OperationName = "CreateMapfix"
|
||||
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
|
||||
CreateScriptOperation OperationName = "CreateScript"
|
||||
@@ -41,9 +40,13 @@ 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"
|
||||
@@ -53,15 +56,16 @@ const (
|
||||
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"
|
||||
SeedCombobulatorOperation OperationName = "SeedCombobulator"
|
||||
SessionRolesOperation OperationName = "SessionRoles"
|
||||
SessionUserOperation OperationName = "SessionUser"
|
||||
SessionValidateOperation OperationName = "SessionValidate"
|
||||
@@ -72,4 +76,5 @@ const (
|
||||
UpdateScriptOperation OperationName = "UpdateScript"
|
||||
UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy"
|
||||
UpdateSubmissionModelOperation OperationName = "UpdateSubmissionModel"
|
||||
UpdateSubmissionReviewOperation OperationName = "UpdateSubmissionReview"
|
||||
)
|
||||
|
||||
@@ -2030,89 +2030,6 @@ func decodeActionSubmissionValidatedParams(args [1]string, argsEscaped bool, r *
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// CombobulateMapParams is parameters of combobulateMap operation.
|
||||
type CombobulateMapParams struct {
|
||||
MapID int64
|
||||
}
|
||||
|
||||
func unpackCombobulateMapParams(packed middleware.Parameters) (params CombobulateMapParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "MapID",
|
||||
In: "path",
|
||||
}
|
||||
params.MapID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeCombobulateMapParams(args [1]string, argsEscaped bool, r *http.Request) (params CombobulateMapParams, _ error) {
|
||||
// Decode path: MapID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "MapID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.MapID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.MapID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "MapID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// CreateMapfixAuditCommentParams is parameters of createMapfixAuditComment operation.
|
||||
type CreateMapfixAuditCommentParams struct {
|
||||
// The unique identifier for a mapfix.
|
||||
@@ -2281,6 +2198,90 @@ func decodeCreateSubmissionAuditCommentParams(args [1]string, argsEscaped bool,
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// CreateSubmissionReviewParams is parameters of createSubmissionReview operation.
|
||||
type CreateSubmissionReviewParams struct {
|
||||
// The unique identifier for a submission.
|
||||
SubmissionID int64
|
||||
}
|
||||
|
||||
func unpackCreateSubmissionReviewParams(packed middleware.Parameters) (params CreateSubmissionReviewParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
}
|
||||
params.SubmissionID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeCreateSubmissionReviewParams(args [1]string, argsEscaped bool, r *http.Request) (params CreateSubmissionReviewParams, _ error) {
|
||||
// Decode path: SubmissionID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "SubmissionID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.SubmissionID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.SubmissionID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// DeleteScriptParams is parameters of deleteScript operation.
|
||||
type DeleteScriptParams struct {
|
||||
// The unique identifier for a script.
|
||||
@@ -2532,6 +2533,172 @@ func decodeDownloadMapAssetParams(args [1]string, argsEscaped bool, r *http.Requ
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// GetAOREventParams is parameters of getAOREvent operation.
|
||||
type GetAOREventParams struct {
|
||||
AOREventID int64
|
||||
}
|
||||
|
||||
func unpackGetAOREventParams(packed middleware.Parameters) (params GetAOREventParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "AOREventID",
|
||||
In: "path",
|
||||
}
|
||||
params.AOREventID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeGetAOREventParams(args [1]string, argsEscaped bool, r *http.Request) (params GetAOREventParams, _ error) {
|
||||
// Decode path: AOREventID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "AOREventID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.AOREventID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 1,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.AOREventID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "AOREventID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// GetAOREventSubmissionsParams is parameters of getAOREventSubmissions operation.
|
||||
type GetAOREventSubmissionsParams struct {
|
||||
AOREventID int64
|
||||
}
|
||||
|
||||
func unpackGetAOREventSubmissionsParams(packed middleware.Parameters) (params GetAOREventSubmissionsParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "AOREventID",
|
||||
In: "path",
|
||||
}
|
||||
params.AOREventID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeGetAOREventSubmissionsParams(args [1]string, argsEscaped bool, r *http.Request) (params GetAOREventSubmissionsParams, _ error) {
|
||||
// Decode path: AOREventID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "AOREventID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.AOREventID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 1,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.AOREventID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "AOREventID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// GetAssetThumbnailParams is parameters of getAssetThumbnail operation.
|
||||
type GetAssetThumbnailParams struct {
|
||||
AssetID uint64
|
||||
@@ -3310,6 +3477,143 @@ func decodeGetUserThumbnailParams(args [1]string, argsEscaped bool, r *http.Requ
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ListAOREventsParams is parameters of listAOREvents operation.
|
||||
type ListAOREventsParams struct {
|
||||
Page int32
|
||||
Limit int32
|
||||
}
|
||||
|
||||
func unpackListAOREventsParams(packed middleware.Parameters) (params ListAOREventsParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "Page",
|
||||
In: "query",
|
||||
}
|
||||
params.Page = packed[key].(int32)
|
||||
}
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "Limit",
|
||||
In: "query",
|
||||
}
|
||||
params.Limit = packed[key].(int32)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeListAOREventsParams(args [0]string, argsEscaped bool, r *http.Request) (params ListAOREventsParams, _ error) {
|
||||
q := uri.NewQueryDecoder(r.URL.Query())
|
||||
// Decode query: Page.
|
||||
if err := func() error {
|
||||
cfg := uri.QueryParameterDecodingConfig{
|
||||
Name: "Page",
|
||||
Style: uri.QueryStyleForm,
|
||||
Explode: true,
|
||||
}
|
||||
|
||||
if err := q.HasParam(cfg); err == nil {
|
||||
if err := q.DecodeParam(cfg, func(d uri.Decoder) error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt32(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.Page = c
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 1,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.Page)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "Page",
|
||||
In: "query",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// Decode query: Limit.
|
||||
if err := func() error {
|
||||
cfg := uri.QueryParameterDecodingConfig{
|
||||
Name: "Limit",
|
||||
Style: uri.QueryStyleForm,
|
||||
Explode: true,
|
||||
}
|
||||
|
||||
if err := q.HasParam(cfg); err == nil {
|
||||
if err := q.DecodeParam(cfg, func(d uri.Decoder) error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt32(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.Limit = c
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: true,
|
||||
Max: 100,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.Limit)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "Limit",
|
||||
In: "query",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ListMapfixAuditEventsParams is parameters of listMapfixAuditEvents operation.
|
||||
type ListMapfixAuditEventsParams struct {
|
||||
// The unique identifier for a mapfix.
|
||||
@@ -5899,6 +6203,90 @@ func decodeListSubmissionAuditEventsParams(args [1]string, argsEscaped bool, r *
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ListSubmissionReviewsParams is parameters of listSubmissionReviews operation.
|
||||
type ListSubmissionReviewsParams struct {
|
||||
// The unique identifier for a submission.
|
||||
SubmissionID int64
|
||||
}
|
||||
|
||||
func unpackListSubmissionReviewsParams(packed middleware.Parameters) (params ListSubmissionReviewsParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
}
|
||||
params.SubmissionID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeListSubmissionReviewsParams(args [1]string, argsEscaped bool, r *http.Request) (params ListSubmissionReviewsParams, _ error) {
|
||||
// Decode path: SubmissionID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "SubmissionID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.SubmissionID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.SubmissionID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// ListSubmissionsParams is parameters of listSubmissions operation.
|
||||
type ListSubmissionsParams struct {
|
||||
Page int32
|
||||
@@ -7570,3 +7958,158 @@ func decodeUpdateSubmissionModelParams(args [1]string, argsEscaped bool, r *http
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// UpdateSubmissionReviewParams is parameters of updateSubmissionReview operation.
|
||||
type UpdateSubmissionReviewParams struct {
|
||||
// The unique identifier for a submission.
|
||||
SubmissionID int64
|
||||
ReviewID int64
|
||||
}
|
||||
|
||||
func unpackUpdateSubmissionReviewParams(packed middleware.Parameters) (params UpdateSubmissionReviewParams) {
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
}
|
||||
params.SubmissionID = packed[key].(int64)
|
||||
}
|
||||
{
|
||||
key := middleware.ParameterKey{
|
||||
Name: "ReviewID",
|
||||
In: "path",
|
||||
}
|
||||
params.ReviewID = packed[key].(int64)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func decodeUpdateSubmissionReviewParams(args [2]string, argsEscaped bool, r *http.Request) (params UpdateSubmissionReviewParams, _ error) {
|
||||
// Decode path: SubmissionID.
|
||||
if err := func() error {
|
||||
param := args[0]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "SubmissionID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.SubmissionID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.SubmissionID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "SubmissionID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// Decode path: ReviewID.
|
||||
if err := func() error {
|
||||
param := args[1]
|
||||
if argsEscaped {
|
||||
unescaped, err := url.PathUnescape(args[1])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unescape path")
|
||||
}
|
||||
param = unescaped
|
||||
}
|
||||
if len(param) > 0 {
|
||||
d := uri.NewPathDecoder(uri.PathDecoderConfig{
|
||||
Param: "ReviewID",
|
||||
Value: param,
|
||||
Style: uri.PathStyleSimple,
|
||||
Explode: false,
|
||||
})
|
||||
|
||||
if err := func() error {
|
||||
val, err := d.DecodeValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := conv.ToInt64(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params.ReviewID = c
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(params.ReviewID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return validate.ErrFieldRequired
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return params, &ogenerrors.DecodeParamError{
|
||||
Name: "ReviewID",
|
||||
In: "path",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
@@ -717,6 +717,85 @@ func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeCreateSubmissionReviewRequest(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)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
|
||||
req []ReleaseInfo,
|
||||
rawBody []byte,
|
||||
@@ -1021,3 +1100,82 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,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,
|
||||
@@ -197,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
|
||||
}
|
||||
|
||||
@@ -1733,66 +1733,6 @@ func decodeBatchUsernamesResponse(resp *http.Response) (res *BatchUsernamesOK, _
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeCombobulateMapResponse(resp *http.Response) (res *CombobulateMapNoContent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
// Code 204.
|
||||
return &CombobulateMapNoContent{}, nil
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeCreateMapfixResponse(resp *http.Response) (res *OperationID, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 201:
|
||||
@@ -2418,6 +2358,107 @@ func decodeCreateSubmissionAuditCommentResponse(resp *http.Response) (res *Creat
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeCreateSubmissionReviewResponse(resp *http.Response) (res *SubmissionReview, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
// Code 200.
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response SubmissionReview
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &response, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeDeleteScriptResponse(resp *http.Response) (res *DeleteScriptNoContent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
@@ -2614,6 +2655,334 @@ func decodeDownloadMapAssetResponse(resp *http.Response) (res DownloadMapAssetOK
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeGetAOREventResponse(resp *http.Response) (res *AOREvent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
// Code 200.
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response AOREvent
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &response, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeGetAOREventSubmissionsResponse(resp *http.Response) (res []Submission, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
// Code 200.
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response []Submission
|
||||
if err := func() error {
|
||||
response = make([]Submission, 0)
|
||||
if err := d.Arr(func(d *jx.Decoder) error {
|
||||
var elem Submission
|
||||
if err := elem.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
response = append(response, elem)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if response == nil {
|
||||
return errors.New("nil is invalid value")
|
||||
}
|
||||
var failures []validate.FieldError
|
||||
for i, elem := range response {
|
||||
if err := func() error {
|
||||
if err := elem.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: fmt.Sprintf("[%d]", i),
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return response, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeGetActiveAOREventResponse(resp *http.Response) (res *AOREvent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
// Code 200.
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response AOREvent
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &response, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeGetAssetThumbnailResponse(resp *http.Response) (res *GetAssetThumbnailFound, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 302:
|
||||
@@ -3519,6 +3888,132 @@ func decodeGetUserThumbnailResponse(resp *http.Response) (res *GetUserThumbnailF
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeListAOREventsResponse(resp *http.Response) (res []AOREvent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
// Code 200.
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response []AOREvent
|
||||
if err := func() error {
|
||||
response = make([]AOREvent, 0)
|
||||
if err := d.Arr(func(d *jx.Decoder) error {
|
||||
var elem AOREvent
|
||||
if err := elem.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
response = append(response, elem)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if response == nil {
|
||||
return errors.New("nil is invalid value")
|
||||
}
|
||||
var failures []validate.FieldError
|
||||
for i, elem := range response {
|
||||
if err := func() error {
|
||||
if err := elem.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: fmt.Sprintf("[%d]", i),
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return response, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeListMapfixAuditEventsResponse(resp *http.Response) (res []AuditEvent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
@@ -4250,6 +4745,132 @@ func decodeListSubmissionAuditEventsResponse(resp *http.Response) (res []AuditEv
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeListSubmissionReviewsResponse(resp *http.Response) (res []SubmissionReview, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
// Code 200.
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response []SubmissionReview
|
||||
if err := func() error {
|
||||
response = make([]SubmissionReview, 0)
|
||||
if err := d.Arr(func(d *jx.Decoder) error {
|
||||
var elem SubmissionReview
|
||||
if err := elem.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
response = append(response, elem)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if response == nil {
|
||||
return errors.New("nil is invalid value")
|
||||
}
|
||||
var failures []validate.FieldError
|
||||
for i, elem := range response {
|
||||
if err := func() error {
|
||||
if err := elem.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: fmt.Sprintf("[%d]", i),
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return response, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeListSubmissionsResponse(resp *http.Response) (res *Submissions, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
@@ -4452,66 +5073,6 @@ func decodeReleaseSubmissionsResponse(resp *http.Response) (res *OperationID, _
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeSeedCombobulatorResponse(resp *http.Response) (res *SeedCombobulatorNoContent, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 204:
|
||||
// Code 204.
|
||||
return &SeedCombobulatorNoContent{}, nil
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeSessionRolesResponse(resp *http.Response) (res *Roles, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
@@ -5227,3 +5788,104 @@ func decodeUpdateSubmissionModelResponse(resp *http.Response) (res *UpdateSubmis
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
func decodeUpdateSubmissionReviewResponse(resp *http.Response) (res *SubmissionReview, _ error) {
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
// Code 200.
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response SubmissionReview
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &response, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}
|
||||
// Convenient error response.
|
||||
defRes, err := func() (res *ErrorStatusCode, err error) {
|
||||
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "parse media type")
|
||||
}
|
||||
switch {
|
||||
case ct == "application/json":
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
d := jx.DecodeBytes(buf)
|
||||
|
||||
var response Error
|
||||
if err := func() error {
|
||||
if err := response.Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.Skip(); err != io.EOF {
|
||||
return errors.New("unexpected trailing data")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
err = &ogenerrors.DecodeBodyError{
|
||||
ContentType: ct,
|
||||
Body: buf,
|
||||
Err: err,
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
// Validate response.
|
||||
if err := func() error {
|
||||
if err := response.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return res, errors.Wrap(err, "validate")
|
||||
}
|
||||
return &ErrorStatusCode{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: response,
|
||||
}, nil
|
||||
default:
|
||||
return res, validate.InvalidContentType(ct)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
|
||||
}
|
||||
return res, errors.Wrap(defRes, "error")
|
||||
}
|
||||
|
||||
@@ -225,13 +225,6 @@ func encodeBatchUsernamesResponse(response *BatchUsernamesOK, w http.ResponseWri
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCombobulateMapResponse(response *CombobulateMapNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
|
||||
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)
|
||||
@@ -316,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))
|
||||
@@ -346,8 +353,53 @@ 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 {
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Location")
|
||||
// Encoding response headers.
|
||||
{
|
||||
h := uri.NewHeaderEncoder(w.Header())
|
||||
@@ -472,7 +524,6 @@ func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, sp
|
||||
}
|
||||
|
||||
func encodeGetUserThumbnailResponse(response *GetUserThumbnailFound, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Location")
|
||||
// Encoding response headers.
|
||||
{
|
||||
h := uri.NewHeaderEncoder(w.Header())
|
||||
@@ -498,6 +549,24 @@ func encodeGetUserThumbnailResponse(response *GetUserThumbnailFound, w http.Resp
|
||||
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)
|
||||
@@ -602,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)
|
||||
@@ -630,13 +717,6 @@ func encodeReleaseSubmissionsResponse(response *OperationID, w http.ResponseWrit
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSeedCombobulatorResponse(response *SeedCombobulatorNoContent, w http.ResponseWriter, span trace.Span) error {
|
||||
w.WriteHeader(204)
|
||||
span.SetStatus(codes.Ok, http.StatusText(204))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeSessionRolesResponse(response *Roles, w http.ResponseWriter, span trace.Span) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -728,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
@@ -15,6 +15,104 @@ func (s *ErrorStatusCode) Error() string {
|
||||
return fmt.Sprintf("code %d: %+v", s.StatusCode, s.Response)
|
||||
}
|
||||
|
||||
// Ref: #/components/schemas/AOREvent
|
||||
type AOREvent struct {
|
||||
ID int64 `json:"ID"`
|
||||
// Unix timestamp for the 1st day of AOR month.
|
||||
StartDate int64 `json:"StartDate"`
|
||||
// Unix timestamp when submissions are frozen.
|
||||
FreezeDate int64 `json:"FreezeDate"`
|
||||
// Unix timestamp when automatic selection occurs (end of week 1).
|
||||
SelectionDate int64 `json:"SelectionDate"`
|
||||
// Unix timestamp when final accept/reject decisions are made (end of month).
|
||||
DecisionDate int64 `json:"DecisionDate"`
|
||||
// AOR Event Status: * `0` - Scheduled * `1` - Open * `2` - Frozen * `3` - Selected * `4` - Completed
|
||||
// * `5` - Closed.
|
||||
Status int32 `json:"Status"`
|
||||
CreatedAt int64 `json:"CreatedAt"`
|
||||
UpdatedAt int64 `json:"UpdatedAt"`
|
||||
}
|
||||
|
||||
// GetID returns the value of ID.
|
||||
func (s *AOREvent) GetID() int64 {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetStartDate returns the value of StartDate.
|
||||
func (s *AOREvent) GetStartDate() int64 {
|
||||
return s.StartDate
|
||||
}
|
||||
|
||||
// GetFreezeDate returns the value of FreezeDate.
|
||||
func (s *AOREvent) GetFreezeDate() int64 {
|
||||
return s.FreezeDate
|
||||
}
|
||||
|
||||
// GetSelectionDate returns the value of SelectionDate.
|
||||
func (s *AOREvent) GetSelectionDate() int64 {
|
||||
return s.SelectionDate
|
||||
}
|
||||
|
||||
// GetDecisionDate returns the value of DecisionDate.
|
||||
func (s *AOREvent) GetDecisionDate() int64 {
|
||||
return s.DecisionDate
|
||||
}
|
||||
|
||||
// GetStatus returns the value of Status.
|
||||
func (s *AOREvent) GetStatus() int32 {
|
||||
return s.Status
|
||||
}
|
||||
|
||||
// GetCreatedAt returns the value of CreatedAt.
|
||||
func (s *AOREvent) GetCreatedAt() int64 {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt returns the value of UpdatedAt.
|
||||
func (s *AOREvent) GetUpdatedAt() int64 {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// SetID sets the value of ID.
|
||||
func (s *AOREvent) SetID(val int64) {
|
||||
s.ID = val
|
||||
}
|
||||
|
||||
// SetStartDate sets the value of StartDate.
|
||||
func (s *AOREvent) SetStartDate(val int64) {
|
||||
s.StartDate = val
|
||||
}
|
||||
|
||||
// SetFreezeDate sets the value of FreezeDate.
|
||||
func (s *AOREvent) SetFreezeDate(val int64) {
|
||||
s.FreezeDate = val
|
||||
}
|
||||
|
||||
// SetSelectionDate sets the value of SelectionDate.
|
||||
func (s *AOREvent) SetSelectionDate(val int64) {
|
||||
s.SelectionDate = val
|
||||
}
|
||||
|
||||
// SetDecisionDate sets the value of DecisionDate.
|
||||
func (s *AOREvent) SetDecisionDate(val int64) {
|
||||
s.DecisionDate = val
|
||||
}
|
||||
|
||||
// SetStatus sets the value of Status.
|
||||
func (s *AOREvent) SetStatus(val int32) {
|
||||
s.Status = val
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the value of CreatedAt.
|
||||
func (s *AOREvent) SetCreatedAt(val int64) {
|
||||
s.CreatedAt = val
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the value of UpdatedAt.
|
||||
func (s *AOREvent) SetUpdatedAt(val int64) {
|
||||
s.UpdatedAt = val
|
||||
}
|
||||
|
||||
// ActionMapfixAcceptedNoContent is response for ActionMapfixAccepted operation.
|
||||
type ActionMapfixAcceptedNoContent struct{}
|
||||
|
||||
@@ -441,9 +539,6 @@ func (s *BatchUsernamesReq) SetUserIds(val []uint64) {
|
||||
s.UserIds = val
|
||||
}
|
||||
|
||||
// CombobulateMapNoContent is response for CombobulateMap operation.
|
||||
type CombobulateMapNoContent struct{}
|
||||
|
||||
type CookieAuth struct {
|
||||
APIKey string
|
||||
Roles []string
|
||||
@@ -1996,9 +2091,6 @@ func (s *ScriptUpdate) SetResourceID(val OptInt64) {
|
||||
s.ResourceID = val
|
||||
}
|
||||
|
||||
// SeedCombobulatorNoContent is response for SeedCombobulator operation.
|
||||
type SeedCombobulatorNoContent struct{}
|
||||
|
||||
// SetMapfixCompletedNoContent is response for SetMapfixCompleted operation.
|
||||
type SetMapfixCompletedNoContent struct{}
|
||||
|
||||
@@ -2240,6 +2332,129 @@ func (s *Submission) SetStatusID(val int32) {
|
||||
s.StatusID = val
|
||||
}
|
||||
|
||||
// Ref: #/components/schemas/SubmissionReview
|
||||
type SubmissionReview struct {
|
||||
ID int64 `json:"ID"`
|
||||
SubmissionID int64 `json:"SubmissionID"`
|
||||
ReviewerID int64 `json:"ReviewerID"`
|
||||
// Whether the reviewer recommends accepting the submission.
|
||||
Recommend bool `json:"Recommend"`
|
||||
// Text description of the review reasoning.
|
||||
Description string `json:"Description"`
|
||||
// Flag indicating if the review is outdated due to submission changes.
|
||||
Outdated bool `json:"Outdated"`
|
||||
CreatedAt int64 `json:"CreatedAt"`
|
||||
UpdatedAt int64 `json:"UpdatedAt"`
|
||||
}
|
||||
|
||||
// GetID returns the value of ID.
|
||||
func (s *SubmissionReview) GetID() int64 {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
// GetSubmissionID returns the value of SubmissionID.
|
||||
func (s *SubmissionReview) GetSubmissionID() int64 {
|
||||
return s.SubmissionID
|
||||
}
|
||||
|
||||
// GetReviewerID returns the value of ReviewerID.
|
||||
func (s *SubmissionReview) GetReviewerID() int64 {
|
||||
return s.ReviewerID
|
||||
}
|
||||
|
||||
// GetRecommend returns the value of Recommend.
|
||||
func (s *SubmissionReview) GetRecommend() bool {
|
||||
return s.Recommend
|
||||
}
|
||||
|
||||
// GetDescription returns the value of Description.
|
||||
func (s *SubmissionReview) GetDescription() string {
|
||||
return s.Description
|
||||
}
|
||||
|
||||
// GetOutdated returns the value of Outdated.
|
||||
func (s *SubmissionReview) GetOutdated() bool {
|
||||
return s.Outdated
|
||||
}
|
||||
|
||||
// GetCreatedAt returns the value of CreatedAt.
|
||||
func (s *SubmissionReview) GetCreatedAt() int64 {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt returns the value of UpdatedAt.
|
||||
func (s *SubmissionReview) GetUpdatedAt() int64 {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// SetID sets the value of ID.
|
||||
func (s *SubmissionReview) SetID(val int64) {
|
||||
s.ID = val
|
||||
}
|
||||
|
||||
// SetSubmissionID sets the value of SubmissionID.
|
||||
func (s *SubmissionReview) SetSubmissionID(val int64) {
|
||||
s.SubmissionID = val
|
||||
}
|
||||
|
||||
// SetReviewerID sets the value of ReviewerID.
|
||||
func (s *SubmissionReview) SetReviewerID(val int64) {
|
||||
s.ReviewerID = val
|
||||
}
|
||||
|
||||
// SetRecommend sets the value of Recommend.
|
||||
func (s *SubmissionReview) SetRecommend(val bool) {
|
||||
s.Recommend = val
|
||||
}
|
||||
|
||||
// SetDescription sets the value of Description.
|
||||
func (s *SubmissionReview) SetDescription(val string) {
|
||||
s.Description = val
|
||||
}
|
||||
|
||||
// SetOutdated sets the value of Outdated.
|
||||
func (s *SubmissionReview) SetOutdated(val bool) {
|
||||
s.Outdated = val
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the value of CreatedAt.
|
||||
func (s *SubmissionReview) SetCreatedAt(val int64) {
|
||||
s.CreatedAt = val
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the value of UpdatedAt.
|
||||
func (s *SubmissionReview) SetUpdatedAt(val int64) {
|
||||
s.UpdatedAt = val
|
||||
}
|
||||
|
||||
// Ref: #/components/schemas/SubmissionReviewCreate
|
||||
type SubmissionReviewCreate struct {
|
||||
// Whether the reviewer recommends accepting the submission.
|
||||
Recommend bool `json:"Recommend"`
|
||||
// Text description of the review reasoning.
|
||||
Description string `json:"Description"`
|
||||
}
|
||||
|
||||
// GetRecommend returns the value of Recommend.
|
||||
func (s *SubmissionReviewCreate) GetRecommend() bool {
|
||||
return s.Recommend
|
||||
}
|
||||
|
||||
// GetDescription returns the value of Description.
|
||||
func (s *SubmissionReviewCreate) GetDescription() string {
|
||||
return s.Description
|
||||
}
|
||||
|
||||
// SetRecommend sets the value of Recommend.
|
||||
func (s *SubmissionReviewCreate) SetRecommend(val bool) {
|
||||
s.Recommend = val
|
||||
}
|
||||
|
||||
// SetDescription sets the value of Description.
|
||||
func (s *SubmissionReviewCreate) SetDescription(val string) {
|
||||
s.Description = val
|
||||
}
|
||||
|
||||
// Ref: #/components/schemas/SubmissionTriggerCreate
|
||||
type SubmissionTriggerCreate struct {
|
||||
AssetID int64 `json:"AssetID"`
|
||||
|
||||
@@ -32,7 +32,6 @@ func findAuthorization(h http.Header, prefix string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// operationRolesCookieAuth is a private map storing roles per operation.
|
||||
var operationRolesCookieAuth = map[string][]string{
|
||||
ActionMapfixAcceptedOperation: []string{},
|
||||
ActionMapfixRejectOperation: []string{},
|
||||
@@ -58,7 +57,6 @@ var operationRolesCookieAuth = map[string][]string{
|
||||
ActionSubmissionTriggerUploadOperation: []string{},
|
||||
ActionSubmissionTriggerValidateOperation: []string{},
|
||||
ActionSubmissionValidatedOperation: []string{},
|
||||
CombobulateMapOperation: []string{},
|
||||
CreateMapfixOperation: []string{},
|
||||
CreateMapfixAuditCommentOperation: []string{},
|
||||
CreateScriptOperation: []string{},
|
||||
@@ -66,12 +64,13 @@ 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{},
|
||||
SeedCombobulatorOperation: []string{},
|
||||
SessionRolesOperation: []string{},
|
||||
SessionUserOperation: []string{},
|
||||
SessionValidateOperation: []string{},
|
||||
@@ -82,27 +81,7 @@ var operationRolesCookieAuth = map[string][]string{
|
||||
UpdateScriptOperation: []string{},
|
||||
UpdateScriptPolicyOperation: []string{},
|
||||
UpdateSubmissionModelOperation: []string{},
|
||||
}
|
||||
|
||||
// GetRolesForCookieAuth returns the required roles for the given operation.
|
||||
//
|
||||
// This is useful for authorization scenarios where you need to know which roles
|
||||
// are required for an operation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// requiredRoles := GetRolesForCookieAuth(AddPetOperation)
|
||||
//
|
||||
// Returns nil if the operation has no role requirements or if the operation is unknown.
|
||||
func GetRolesForCookieAuth(operation string) []string {
|
||||
roles, ok := operationRolesCookieAuth[operation]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Return a copy to prevent external modification
|
||||
result := make([]string, len(roles))
|
||||
copy(result, roles)
|
||||
return result
|
||||
UpdateSubmissionReviewOperation: []string{},
|
||||
}
|
||||
|
||||
func (s *Server) securityCookieAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
|
||||
|
||||
@@ -173,12 +173,6 @@ type Handler interface {
|
||||
//
|
||||
// POST /usernames
|
||||
BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (*BatchUsernamesOK, error)
|
||||
// CombobulateMap implements combobulateMap operation.
|
||||
//
|
||||
// Queue a map for combobulator processing.
|
||||
//
|
||||
// POST /maps/{MapID}/combobulate
|
||||
CombobulateMap(ctx context.Context, params CombobulateMapParams) error
|
||||
// CreateMapfix implements createMapfix operation.
|
||||
//
|
||||
// Trigger the validator to create a mapfix.
|
||||
@@ -221,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.
|
||||
@@ -239,6 +239,24 @@ 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.
|
||||
@@ -293,6 +311,12 @@ type Handler interface {
|
||||
//
|
||||
// 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.
|
||||
@@ -329,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.
|
||||
@@ -341,12 +371,6 @@ type Handler interface {
|
||||
//
|
||||
// POST /release-submissions
|
||||
ReleaseSubmissions(ctx context.Context, req []ReleaseInfo) (*OperationID, error)
|
||||
// SeedCombobulator implements seedCombobulator operation.
|
||||
//
|
||||
// Queue all maps for combobulator processing.
|
||||
//
|
||||
// POST /maps-admin/seed-combobulator
|
||||
SeedCombobulator(ctx context.Context) error
|
||||
// SessionRoles implements sessionRoles operation.
|
||||
//
|
||||
// Get list of roles for the current session.
|
||||
@@ -407,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.
|
||||
|
||||
@@ -259,15 +259,6 @@ func (UnimplementedHandler) BatchUsernames(ctx context.Context, req *BatchUserna
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CombobulateMap implements combobulateMap operation.
|
||||
//
|
||||
// Queue a map for combobulator processing.
|
||||
//
|
||||
// POST /maps/{MapID}/combobulate
|
||||
func (UnimplementedHandler) CombobulateMap(ctx context.Context, params CombobulateMapParams) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// CreateMapfix implements createMapfix operation.
|
||||
//
|
||||
// Trigger the validator to create a mapfix.
|
||||
@@ -331,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.
|
||||
@@ -358,6 +358,33 @@ 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.
|
||||
@@ -439,6 +466,15 @@ func (UnimplementedHandler) GetUserThumbnail(ctx context.Context, params GetUser
|
||||
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.
|
||||
@@ -493,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.
|
||||
@@ -511,15 +556,6 @@ func (UnimplementedHandler) ReleaseSubmissions(ctx context.Context, req []Releas
|
||||
return r, ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// SeedCombobulator implements seedCombobulator operation.
|
||||
//
|
||||
// Queue all maps for combobulator processing.
|
||||
//
|
||||
// POST /maps-admin/seed-combobulator
|
||||
func (UnimplementedHandler) SeedCombobulator(ctx context.Context) error {
|
||||
return ht.ErrNotImplemented
|
||||
}
|
||||
|
||||
// SessionRoles implements sessionRoles operation.
|
||||
//
|
||||
// Get list of roles for the current session.
|
||||
@@ -610,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.
|
||||
|
||||
@@ -9,6 +9,39 @@ import (
|
||||
"github.com/ogen-go/ogen/validate"
|
||||
)
|
||||
|
||||
func (s *AOREvent) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: true,
|
||||
Max: 5,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(s.Status)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "Status",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuditEvent) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
@@ -2311,6 +2344,181 @@ func (s *Submission) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubmissionReview) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(s.ID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "ID",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(s.SubmissionID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "SubmissionID",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(s.ReviewerID)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "ReviewerID",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.String{
|
||||
MinLength: 0,
|
||||
MinLengthSet: false,
|
||||
MaxLength: 2048,
|
||||
MaxLengthSet: true,
|
||||
Email: false,
|
||||
Hostname: false,
|
||||
Regex: nil,
|
||||
MinNumeric: 0,
|
||||
MinNumericSet: false,
|
||||
MaxNumeric: 0,
|
||||
MaxNumericSet: false,
|
||||
}).Validate(string(s.Description)); err != nil {
|
||||
return errors.Wrap(err, "string")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "Description",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(s.CreatedAt)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "CreatedAt",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if err := func() error {
|
||||
if err := (validate.Int{
|
||||
MinSet: true,
|
||||
Min: 0,
|
||||
MaxSet: false,
|
||||
Max: 0,
|
||||
MinExclusive: false,
|
||||
MaxExclusive: false,
|
||||
MultipleOfSet: false,
|
||||
MultipleOf: 0,
|
||||
Pattern: nil,
|
||||
}).Validate(int64(s.UpdatedAt)); err != nil {
|
||||
return errors.Wrap(err, "int")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "UpdatedAt",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubmissionReviewCreate) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
}
|
||||
|
||||
var failures []validate.FieldError
|
||||
if err := func() error {
|
||||
if err := (validate.String{
|
||||
MinLength: 0,
|
||||
MinLengthSet: false,
|
||||
MaxLength: 2048,
|
||||
MaxLengthSet: true,
|
||||
Email: false,
|
||||
Hostname: false,
|
||||
Regex: nil,
|
||||
MinNumeric: 0,
|
||||
MinNumericSet: false,
|
||||
MaxNumeric: 0,
|
||||
MaxNumericSet: false,
|
||||
}).Validate(string(s.Description)); err != nil {
|
||||
return errors.Wrap(err, "string")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
failures = append(failures, validate.FieldError{
|
||||
Name: "Description",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return &validate.Error{Fields: failures}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SubmissionTriggerCreate) Validate() error {
|
||||
if s == nil {
|
||||
return validate.ErrNilPointer
|
||||
|
||||
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
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"git.itzana.me/strafesnet/go-grpc/auth"
|
||||
"git.itzana.me/strafesnet/go-grpc/maps"
|
||||
"git.itzana.me/strafesnet/go-grpc/maps_extended"
|
||||
"git.itzana.me/strafesnet/go-grpc/mapfixes"
|
||||
"git.itzana.me/strafesnet/go-grpc/submissions"
|
||||
"git.itzana.me/strafesnet/go-grpc/users"
|
||||
"git.itzana.me/strafesnet/go-grpc/validator"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||
@@ -19,8 +17,6 @@ import (
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/validator_controller"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/web_api"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/redis/go-redis/v9"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -125,12 +121,6 @@ func NewServeCommand() *cli.Command {
|
||||
EnvVars: []string{"REDIS_DB"},
|
||||
Value: 0,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "s3-bucket",
|
||||
Usage: "S3 bucket for map assets",
|
||||
EnvVars: []string{"S3_BUCKET"},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -152,7 +142,7 @@ func serve(ctx *cli.Context) error {
|
||||
_, err = js.AddStream(&nats.StreamConfig{
|
||||
Name: "maptest",
|
||||
Subjects: []string{"maptest.>"},
|
||||
Retention: nats.InterestPolicy,
|
||||
Retention: nats.WorkQueuePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("failed to add stream")
|
||||
@@ -176,13 +166,6 @@ func serve(ctx *cli.Context) error {
|
||||
ApiKey: ctx.String("rbx-api-key"),
|
||||
}
|
||||
|
||||
// Initialize S3 client
|
||||
awsCfg, err := awsconfig.LoadDefaultConfig(ctx.Context)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("failed to load AWS config")
|
||||
}
|
||||
s3Client := s3.NewFromConfig(awsCfg)
|
||||
|
||||
// connect to main game database
|
||||
conn, err := grpc.Dial(ctx.String("data-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
@@ -195,8 +178,6 @@ func serve(ctx *cli.Context) error {
|
||||
users.NewUsersServiceClient(conn),
|
||||
robloxClient,
|
||||
redisClient,
|
||||
s3Client,
|
||||
ctx.String("s3-bucket"),
|
||||
)
|
||||
|
||||
svc_external := web_api.NewService(
|
||||
@@ -223,11 +204,7 @@ func serve(ctx *cli.Context) error {
|
||||
grpcServer := grpc.NewServer()
|
||||
|
||||
maps_controller := controller.NewMapsController(&svc_inner)
|
||||
mapfixes_controller := controller.NewMapfixesController(&svc_inner)
|
||||
submissions_controller := controller.NewSubmissionsController(&svc_inner)
|
||||
maps_extended.RegisterMapsServiceServer(grpcServer,&maps_controller)
|
||||
mapfixes.RegisterMapfixesServiceServer(grpcServer,&mapfixes_controller)
|
||||
submissions.RegisterSubmissionsServiceServer(grpcServer,&submissions_controller)
|
||||
|
||||
mapfix_controller := validator_controller.NewMapfixesController(&svc_inner)
|
||||
operation_controller := validator_controller.NewOperationsController(&svc_inner)
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/go-grpc/mapfixes"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
)
|
||||
|
||||
type Mapfixes struct {
|
||||
*mapfixes.UnimplementedMapfixesServiceServer
|
||||
inner *service.Service
|
||||
}
|
||||
|
||||
func NewMapfixesController(
|
||||
inner *service.Service,
|
||||
) Mapfixes {
|
||||
return Mapfixes{
|
||||
inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *Mapfixes) Get(ctx context.Context, request *mapfixes.MapfixId) (*mapfixes.MapfixResponse, error) {
|
||||
item, err := svc.inner.GetMapfix(ctx, request.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var validated_asset_id *uint64
|
||||
if item.ValidatedAssetID != 0 {
|
||||
validated_asset_id = &item.ValidatedAssetID
|
||||
}
|
||||
var validated_asset_version *uint64
|
||||
if item.ValidatedAssetVersion != 0 {
|
||||
validated_asset_version = &item.ValidatedAssetVersion
|
||||
}
|
||||
return &mapfixes.MapfixResponse{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: uint32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: uint64(item.Submitter),
|
||||
AssetVersion: uint64(item.AssetVersion),
|
||||
AssetID: item.AssetID,
|
||||
ValidatedAssetID: validated_asset_id,
|
||||
ValidatedAssetVersion: validated_asset_version,
|
||||
TargetAssetID: item.TargetAssetID,
|
||||
StatusID: mapfixes.MapfixStatus(item.StatusID),
|
||||
}, nil
|
||||
}
|
||||
func (svc *Mapfixes) GetList(ctx context.Context, request *mapfixes.MapfixIdList) (*mapfixes.MapfixList, error) {
|
||||
items, err := svc.inner.GetMapfixList(ctx, request.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := mapfixes.MapfixList{}
|
||||
resp.Mapfixes = make([]*mapfixes.MapfixResponse, len(items))
|
||||
for i, item := range items {
|
||||
var validated_asset_id *uint64
|
||||
if item.ValidatedAssetID != 0 {
|
||||
validated_asset_id = &item.ValidatedAssetID
|
||||
}
|
||||
var validated_asset_version *uint64
|
||||
if item.ValidatedAssetVersion != 0 {
|
||||
validated_asset_version = &item.ValidatedAssetVersion
|
||||
}
|
||||
resp.Mapfixes[i] = &mapfixes.MapfixResponse{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: uint32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: uint64(item.Submitter),
|
||||
AssetVersion: uint64(item.AssetVersion),
|
||||
AssetID: item.AssetID,
|
||||
ValidatedAssetID: validated_asset_id,
|
||||
ValidatedAssetVersion: validated_asset_version,
|
||||
TargetAssetID: item.TargetAssetID,
|
||||
StatusID: mapfixes.MapfixStatus(item.StatusID),
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
func (svc *Mapfixes) List(ctx context.Context, request *mapfixes.ListRequest) (*mapfixes.MapfixList, error) {
|
||||
if request.Page == nil {
|
||||
return nil, PageError
|
||||
}
|
||||
|
||||
filter := service.NewMapfixFilter()
|
||||
if request.Filter != nil {
|
||||
if request.Filter.DisplayName != nil {
|
||||
filter.SetDisplayName(*request.Filter.DisplayName)
|
||||
}
|
||||
if request.Filter.Creator != nil {
|
||||
filter.SetCreator(*request.Filter.Creator)
|
||||
}
|
||||
if request.Filter.GameID != nil {
|
||||
filter.SetGameID(*request.Filter.GameID)
|
||||
}
|
||||
if request.Filter.Submitter != nil {
|
||||
filter.SetSubmitter(*request.Filter.Submitter)
|
||||
}
|
||||
}
|
||||
|
||||
items, err := svc.inner.ListMapfixes(ctx, filter, model.Page{
|
||||
Number: int32(request.Page.Number),
|
||||
Size: int32(request.Page.Size),
|
||||
}, datastore.ListSortDateDescending)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := mapfixes.MapfixList{}
|
||||
resp.Mapfixes = make([]*mapfixes.MapfixResponse, len(items))
|
||||
for i, item := range items {
|
||||
var validated_asset_id *uint64
|
||||
if item.ValidatedAssetID != 0 {
|
||||
validated_asset_id = &item.ValidatedAssetID
|
||||
}
|
||||
var validated_asset_version *uint64
|
||||
if item.ValidatedAssetVersion != 0 {
|
||||
validated_asset_version = &item.ValidatedAssetVersion
|
||||
}
|
||||
resp.Mapfixes[i] = &mapfixes.MapfixResponse{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: uint32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: uint64(item.Submitter),
|
||||
AssetVersion: uint64(item.AssetVersion),
|
||||
AssetID: item.AssetID,
|
||||
ValidatedAssetID: validated_asset_id,
|
||||
ValidatedAssetVersion: validated_asset_version,
|
||||
TargetAssetID: item.TargetAssetID,
|
||||
StatusID: mapfixes.MapfixStatus(item.StatusID),
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
@@ -195,13 +195,3 @@ func (svc *Maps) IncrementLoadCount(ctx context.Context, request *maps_extended.
|
||||
}
|
||||
return &maps_extended.NullResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Maps) GetSnfmDownloadUrl(ctx context.Context, request *maps_extended.MapId) (*maps_extended.SnfmDownloadUrl, error) {
|
||||
url, err := svc.inner.GetSnfmDownloadUrl(ctx, request.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &maps_extended.SnfmDownloadUrl{
|
||||
Url: url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.itzana.me/strafesnet/go-grpc/submissions"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
)
|
||||
|
||||
type Submissions struct {
|
||||
*submissions.UnimplementedSubmissionsServiceServer
|
||||
inner *service.Service
|
||||
}
|
||||
|
||||
func NewSubmissionsController(
|
||||
inner *service.Service,
|
||||
) Submissions {
|
||||
return Submissions{
|
||||
inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *Submissions) Get(ctx context.Context, request *submissions.SubmissionId) (*submissions.SubmissionResponse, error) {
|
||||
item, err := svc.inner.GetSubmission(ctx, request.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var validated_asset_id *uint64
|
||||
if item.ValidatedAssetID != 0 {
|
||||
validated_asset_id = &item.ValidatedAssetID
|
||||
}
|
||||
var validated_asset_version *uint64
|
||||
if item.ValidatedAssetVersion != 0 {
|
||||
validated_asset_version = &item.ValidatedAssetVersion
|
||||
}
|
||||
var uploaded_asset_id *uint64
|
||||
if item.UploadedAssetID != 0 {
|
||||
uploaded_asset_id = &item.UploadedAssetID
|
||||
}
|
||||
return &submissions.SubmissionResponse{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: uint32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: uint64(item.Submitter),
|
||||
AssetVersion: uint64(item.AssetVersion),
|
||||
AssetID: item.AssetID,
|
||||
ValidatedAssetID: validated_asset_id,
|
||||
ValidatedAssetVersion: validated_asset_version,
|
||||
UploadedAssetID: uploaded_asset_id,
|
||||
StatusID: submissions.SubmissionStatus(item.StatusID),
|
||||
}, nil
|
||||
}
|
||||
func (svc *Submissions) GetList(ctx context.Context, request *submissions.SubmissionIdList) (*submissions.SubmissionList, error) {
|
||||
items, err := svc.inner.GetSubmissionList(ctx, request.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := submissions.SubmissionList{}
|
||||
resp.Submissions = make([]*submissions.SubmissionResponse, len(items))
|
||||
for i, item := range items {
|
||||
var validated_asset_id *uint64
|
||||
if item.ValidatedAssetID != 0 {
|
||||
validated_asset_id = &item.ValidatedAssetID
|
||||
}
|
||||
var validated_asset_version *uint64
|
||||
if item.ValidatedAssetVersion != 0 {
|
||||
validated_asset_version = &item.ValidatedAssetVersion
|
||||
}
|
||||
var uploaded_asset_id *uint64
|
||||
if item.UploadedAssetID != 0 {
|
||||
uploaded_asset_id = &item.UploadedAssetID
|
||||
}
|
||||
resp.Submissions[i] = &submissions.SubmissionResponse{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: uint32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: uint64(item.Submitter),
|
||||
AssetVersion: uint64(item.AssetVersion),
|
||||
AssetID: item.AssetID,
|
||||
ValidatedAssetID: validated_asset_id,
|
||||
ValidatedAssetVersion: validated_asset_version,
|
||||
UploadedAssetID: uploaded_asset_id,
|
||||
StatusID: submissions.SubmissionStatus(item.StatusID),
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
func (svc *Submissions) List(ctx context.Context, request *submissions.ListRequest) (*submissions.SubmissionList, error) {
|
||||
if request.Page == nil {
|
||||
return nil, PageError
|
||||
}
|
||||
|
||||
filter := service.NewSubmissionFilter()
|
||||
if request.Filter != nil {
|
||||
if request.Filter.DisplayName != nil {
|
||||
filter.SetDisplayName(*request.Filter.DisplayName)
|
||||
}
|
||||
if request.Filter.Creator != nil {
|
||||
filter.SetCreator(*request.Filter.Creator)
|
||||
}
|
||||
if request.Filter.GameID != nil {
|
||||
filter.SetGameID(*request.Filter.GameID)
|
||||
}
|
||||
if request.Filter.Submitter != nil {
|
||||
filter.SetSubmitter(*request.Filter.Submitter)
|
||||
}
|
||||
}
|
||||
|
||||
items, err := svc.inner.ListSubmissions(ctx, filter, model.Page{
|
||||
Number: int32(request.Page.Number),
|
||||
Size: int32(request.Page.Size),
|
||||
}, datastore.ListSortDateDescending)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := submissions.SubmissionList{}
|
||||
resp.Submissions = make([]*submissions.SubmissionResponse, len(items))
|
||||
for i, item := range items {
|
||||
var validated_asset_id *uint64
|
||||
if item.ValidatedAssetID != 0 {
|
||||
validated_asset_id = &item.ValidatedAssetID
|
||||
}
|
||||
var validated_asset_version *uint64
|
||||
if item.ValidatedAssetVersion != 0 {
|
||||
validated_asset_version = &item.ValidatedAssetVersion
|
||||
}
|
||||
var uploaded_asset_id *uint64
|
||||
if item.UploadedAssetID != 0 {
|
||||
uploaded_asset_id = &item.UploadedAssetID
|
||||
}
|
||||
resp.Submissions[i] = &submissions.SubmissionResponse{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: uint32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: uint64(item.Submitter),
|
||||
AssetVersion: uint64(item.AssetVersion),
|
||||
AssetID: item.AssetID,
|
||||
ValidatedAssetID: validated_asset_id,
|
||||
ValidatedAssetVersion: validated_asset_version,
|
||||
UploadedAssetID: uploaded_asset_id,
|
||||
StatusID: submissions.SubmissionStatus(item.StatusID),
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -47,7 +50,6 @@ type Maps interface {
|
||||
Create(ctx context.Context, smap model.Map) (model.Map, error)
|
||||
Update(ctx context.Context, id int64, values OptionalMap) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
GetAll(ctx context.Context) ([]model.Map, error)
|
||||
List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.Map, error)
|
||||
IncrementLoadCount(ctx context.Context, id int64) error
|
||||
}
|
||||
@@ -84,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)
|
||||
@@ -100,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)
|
||||
}
|
||||
|
||||
@@ -23,14 +23,6 @@ func (q OptionalMap) AddNotNil(column string, value interface{}) OptionalMap {
|
||||
return q
|
||||
}
|
||||
|
||||
func (q OptionalMap) Pop(column string) (interface{}, bool) {
|
||||
value, ok := q.filter[column]
|
||||
if ok {
|
||||
delete(q.filter, column)
|
||||
}
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (q OptionalMap) Map() map[string]interface{} {
|
||||
return q.filter
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -74,21 +74,9 @@ func (env *Maps) Delete(ctx context.Context, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (env *Maps) GetAll(ctx context.Context) ([]model.Map, error) {
|
||||
var maps []model.Map
|
||||
if err := env.db.Find(&maps).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return maps, nil
|
||||
}
|
||||
|
||||
func (env *Maps) List(ctx context.Context, filters datastore.OptionalMap, page model.Page) ([]model.Map, error) {
|
||||
var events []model.Map
|
||||
tx := env.db.Model(&model.Map{})
|
||||
if displayName, ok := filters.Pop("display_name"); ok {
|
||||
tx = tx.Where("display_name ILIKE ?", "%"+displayName.(string)+"%")
|
||||
}
|
||||
if err := tx.Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&events).Error; err != nil {
|
||||
if err := env.db.Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
@@ -91,7 +91,3 @@ type ReleaseMapfixRequest struct {
|
||||
ModelVersion uint64
|
||||
TargetAssetID uint64
|
||||
}
|
||||
|
||||
type SeedCombobulatorRequest struct {
|
||||
AssetID uint64
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.itzana.me/strafesnet/go-grpc/maps_extended"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MapFilter struct {
|
||||
|
||||
@@ -81,47 +81,6 @@ func (h *MapHandler) Get(ctx *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Download SNFM file
|
||||
// @Description Redirects to a signed download URL for a map's SNFM file
|
||||
// @Tags maps
|
||||
// @Security ApiKeyAuth
|
||||
// @Param id path int true "Map ID"
|
||||
// @Success 307 "Redirect to signed S3 URL"
|
||||
// @Failure 404 {object} dto.Error "Map not found"
|
||||
// @Failure default {object} dto.Error "General error response"
|
||||
// @Router /map/{id}/snfm [get]
|
||||
func (h *MapHandler) GetSnfmDownloadUrl(ctx *gin.Context) {
|
||||
id := ctx.Param("id")
|
||||
mapID, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, dto.Error{
|
||||
Error: "Invalid map ID format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := maps_extended.NewMapsServiceClient(h.mapsClient).GetSnfmDownloadUrl(ctx, &maps_extended.MapId{
|
||||
ID: mapID,
|
||||
})
|
||||
if err != nil {
|
||||
statusCode := http.StatusInternalServerError
|
||||
errorMessage := "Failed to get download URL"
|
||||
|
||||
if status.Code(err) == codes.NotFound {
|
||||
statusCode = http.StatusNotFound
|
||||
errorMessage = "Map not found"
|
||||
}
|
||||
|
||||
ctx.JSON(statusCode, dto.Error{
|
||||
Error: errorMessage,
|
||||
})
|
||||
log.WithError(err).Error("Failed to get SNFM download URL")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(http.StatusTemporaryRedirect, resp.Url)
|
||||
}
|
||||
|
||||
// @Summary List maps
|
||||
// @Description Get a list of maps
|
||||
// @Tags maps
|
||||
|
||||
@@ -93,13 +93,6 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
|
||||
v1.GET("/map/:id", mapsHandler.Get)
|
||||
}
|
||||
|
||||
v1Download := public_api.Group("/v1")
|
||||
{
|
||||
v1Download.Use(middleware.ValidateRequest("Maps", "Download", cfg.devClient))
|
||||
|
||||
v1Download.GET("/map/:id/snfm", mapsHandler.GetSnfmDownloadUrl)
|
||||
}
|
||||
|
||||
// Docs
|
||||
public_api.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||
public_api.GET("/", func(ctx *gin.Context) {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -103,10 +103,6 @@ func (svc *Service) GetMapfix(ctx context.Context, id int64) (model.Mapfix, erro
|
||||
return svc.db.Mapfixes().Get(ctx, id)
|
||||
}
|
||||
|
||||
func (svc *Service) GetMapfixList(ctx context.Context, ids []int64) ([]model.Mapfix, error) {
|
||||
return svc.db.Mapfixes().GetList(ctx, ids)
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateMapfix(ctx context.Context, id int64, pmap MapfixUpdate) error {
|
||||
return svc.db.Mapfixes().Update(ctx, id, datastore.OptionalMap(pmap))
|
||||
}
|
||||
|
||||
@@ -99,10 +99,6 @@ func (svc *Service) CreateMap(ctx context.Context, item model.Map) (int64, error
|
||||
return map_item.ID, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetAllMaps(ctx context.Context) ([]model.Map, error) {
|
||||
return svc.db.Maps().GetAll(ctx)
|
||||
}
|
||||
|
||||
func (svc *Service) ListMaps(ctx context.Context, filter MapFilter, page model.Page) ([]model.Map, error) {
|
||||
return svc.db.Maps().List(ctx, datastore.OptionalMap(filter), page)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/model"
|
||||
)
|
||||
|
||||
func (svc *Service) NatsSeedCombobulator(assetID uint64) error {
|
||||
request := model.SeedCombobulatorRequest{
|
||||
AssetID: assetID,
|
||||
}
|
||||
|
||||
j, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = svc.nats.Publish("maptest.combobulator.seed", j)
|
||||
return err
|
||||
}
|
||||
@@ -2,14 +2,11 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
@@ -20,8 +17,6 @@ type Service struct {
|
||||
maps maps.MapsServiceClient
|
||||
users users.UsersServiceClient
|
||||
thumbnailService *ThumbnailService
|
||||
s3Presign *s3.PresignClient
|
||||
s3Bucket string
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@@ -31,8 +26,6 @@ func NewService(
|
||||
users users.UsersServiceClient,
|
||||
robloxClient *roblox.Client,
|
||||
redisClient *redis.Client,
|
||||
s3Client *s3.Client,
|
||||
s3Bucket string,
|
||||
) Service {
|
||||
return Service{
|
||||
db: db,
|
||||
@@ -40,23 +33,9 @@ func NewService(
|
||||
maps: maps,
|
||||
users: users,
|
||||
thumbnailService: NewThumbnailService(robloxClient, redisClient),
|
||||
s3Presign: s3.NewPresignClient(s3Client),
|
||||
s3Bucket: s3Bucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetSnfmDownloadUrl(ctx context.Context, mapID int64) (string, error) {
|
||||
key := fmt.Sprintf("maps/%d.snfm", mapID)
|
||||
presigned, err := s.s3Presign.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: &s.s3Bucket,
|
||||
Key: &key,
|
||||
}, s3.WithPresignExpires(5*time.Minute))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return presigned.URL, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
@@ -34,10 +35,10 @@ var(
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCreationPhaseMapfixesLimit = fmt.Errorf("%w: Active mapfixes limited to 20", ErrPermissionDenied)
|
||||
ErrActiveMapfixSameTargetAssetID = fmt.Errorf("%w: There is an active mapfix for this map already", ErrPermissionDenied)
|
||||
ErrCreationPhaseMapfixesLimit = errors.New("Active mapfixes limited to 20")
|
||||
ErrActiveMapfixSameTargetAssetID = errors.New("There is an active mapfix for this map already")
|
||||
ErrAcceptOwnMapfix = fmt.Errorf("%w: You cannot accept your own mapfix as the submitter", ErrPermissionDenied)
|
||||
ErrCreateMapfixRateLimit = fmt.Errorf("%w: You must not create more than 5 mapfixes every 10 minutes", ErrTooManyRequests)
|
||||
ErrCreateMapfixRateLimit = errors.New("You must not create more than 5 mapfixes every 10 minutes")
|
||||
)
|
||||
|
||||
// POST /mapfixes
|
||||
@@ -447,12 +448,7 @@ func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params api.A
|
||||
target_status := model.MapfixStatusChangesRequested
|
||||
update := service.NewMapfixUpdate()
|
||||
update.SetStatusID(target_status)
|
||||
allow_statuses := []model.MapfixStatus{
|
||||
model.MapfixStatusUploaded,
|
||||
model.MapfixStatusValidated,
|
||||
model.MapfixStatusAcceptedUnvalidated,
|
||||
model.MapfixStatusSubmitted,
|
||||
}
|
||||
allow_statuses := []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAcceptedUnvalidated, model.MapfixStatusSubmitted}
|
||||
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -558,11 +554,7 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
|
||||
target_status := model.MapfixStatusSubmitting
|
||||
update := service.NewMapfixUpdate()
|
||||
update.SetStatusID(target_status)
|
||||
allow_statuses := []model.MapfixStatus{
|
||||
model.MapfixStatusUnderConstruction,
|
||||
model.MapfixStatusChangesRequested,
|
||||
model.MapfixStatusSubmitted,
|
||||
}
|
||||
allow_statuses := []model.MapfixStatus{model.MapfixStatusUnderConstruction, model.MapfixStatusChangesRequested}
|
||||
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -86,61 +86,6 @@ func (svc *Service) GetMap(ctx context.Context, params api.GetMapParams) (*api.M
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SeedCombobulator implements seedCombobulator operation.
|
||||
//
|
||||
// Queue all maps for combobulator processing.
|
||||
//
|
||||
// POST /maps-admin/seed-combobulator
|
||||
func (svc *Service) SeedCombobulator(ctx context.Context) error {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return ErrUserInfo
|
||||
}
|
||||
|
||||
has_role, err := userInfo.HasRoleSubmissionRelease()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has_role {
|
||||
return ErrPermissionDeniedNeedRoleSubmissionRelease
|
||||
}
|
||||
|
||||
maps, err := svc.inner.GetAllMaps(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, m := range maps {
|
||||
if err := svc.inner.NatsSeedCombobulator(uint64(m.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CombobulateMap implements combobulateMap operation.
|
||||
//
|
||||
// Queue a map for combobulator processing.
|
||||
//
|
||||
// POST /maps/{MapID}/combobulate
|
||||
func (svc *Service) CombobulateMap(ctx context.Context, params api.CombobulateMapParams) error {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return ErrUserInfo
|
||||
}
|
||||
|
||||
has_role, err := userInfo.HasRoleSubmissionRelease()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has_role {
|
||||
return ErrPermissionDeniedNeedRoleSubmissionRelease
|
||||
}
|
||||
|
||||
return svc.inner.NatsSeedCombobulator(uint64(params.MapID));
|
||||
}
|
||||
|
||||
// DownloadMapAsset invokes downloadMapAsset operation.
|
||||
//
|
||||
// Download the map asset.
|
||||
|
||||
@@ -2,7 +2,7 @@ package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
"git.itzana.me/strafesnet/go-grpc/auth"
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/api"
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
|
||||
var (
|
||||
// ErrMissingSessionID there is no session id
|
||||
ErrMissingSessionID = fmt.Errorf("%w: SessionID missing", ErrUserInfo)
|
||||
ErrMissingSessionID = errors.New("SessionID missing")
|
||||
// ErrInvalidSession caller does not have a valid session
|
||||
ErrInvalidSession = fmt.Errorf("%w: Session invalid", ErrUserInfo)
|
||||
ErrInvalidSession = errors.New("Session invalid")
|
||||
)
|
||||
|
||||
type UserInfoHandle struct {
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadRequest = errors.New("Bad request")
|
||||
ErrTooManyRequests = errors.New("Too many requests")
|
||||
// ErrPermissionDenied caller does not have the required role
|
||||
ErrPermissionDenied = errors.New("Permission denied")
|
||||
// ErrUserInfo user info is missing for some reason
|
||||
@@ -28,7 +26,7 @@ var (
|
||||
ErrPermissionDeniedNeedRoleMapDownload = fmt.Errorf("%w: Need Role MapDownload", ErrPermissionDenied)
|
||||
ErrPermissionDeniedNeedRoleScriptWrite = fmt.Errorf("%w: Need Role ScriptWrite", ErrPermissionDenied)
|
||||
ErrPermissionDeniedNeedRoleMaptest = fmt.Errorf("%w: Need Role Maptest", ErrPermissionDenied)
|
||||
ErrNegativeID = fmt.Errorf("%w: A negative ID was provided", ErrBadRequest)
|
||||
ErrNegativeID = errors.New("A negative ID was provided")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -51,20 +49,14 @@ func NewService(
|
||||
// Used for common default response.
|
||||
func (svc *Service) NewError(ctx context.Context, err error) *api.ErrorStatusCode {
|
||||
status := 500
|
||||
if errors.Is(err, ErrBadRequest) {
|
||||
status = 400
|
||||
}
|
||||
if errors.Is(err, ErrUserInfo) {
|
||||
status = 401
|
||||
if errors.Is(err, datastore.ErrNotExist) {
|
||||
status = 404
|
||||
}
|
||||
if errors.Is(err, ErrPermissionDenied) {
|
||||
status = 403
|
||||
}
|
||||
if errors.Is(err, datastore.ErrNotExist) {
|
||||
status = 404
|
||||
}
|
||||
if errors.Is(err, ErrTooManyRequests) {
|
||||
status = 429
|
||||
if errors.Is(err, ErrUserInfo) {
|
||||
status = 401
|
||||
}
|
||||
return &api.ErrorStatusCode{
|
||||
StatusCode: status,
|
||||
|
||||
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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package web_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
@@ -12,35 +13,34 @@ import (
|
||||
"git.itzana.me/strafesnet/maps-service/pkg/service"
|
||||
)
|
||||
|
||||
var (
|
||||
CreationPhaseSubmissionsLimit = 20
|
||||
var(
|
||||
CreationPhaseSubmissionsLimit = 20
|
||||
CreationPhaseSubmissionStatuses = []model.SubmissionStatus{
|
||||
model.SubmissionStatusChangesRequested,
|
||||
model.SubmissionStatusSubmitted,
|
||||
model.SubmissionStatusUnderConstruction,
|
||||
}
|
||||
// Allow 5 submissions every 10 minutes
|
||||
CreateSubmissionRateLimit int64 = 5
|
||||
CreateSubmissionRecencyWindow = time.Second * 600
|
||||
CreateSubmissionRateLimit int64 = 5
|
||||
CreateSubmissionRecencyWindow = time.Second*600
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCreationPhaseSubmissionsLimit = fmt.Errorf("%w: Active submissions limited to 20", ErrPermissionDenied)
|
||||
ErrUploadedAssetIDAlreadyExists = fmt.Errorf("%w: The submission UploadedAssetID is already set", ErrPermissionDenied)
|
||||
ErrReleaseInvalidStatus = fmt.Errorf("%w: Only submissions with Uploaded status can be released", ErrPermissionDenied)
|
||||
ErrReleaseNoUploadedAssetID = fmt.Errorf("%w: Only submissions with a UploadedAssetID can be released", ErrPermissionDenied)
|
||||
ErrAcceptOwnSubmission = fmt.Errorf("%w: You cannot accept your own submission as the submitter", ErrPermissionDenied)
|
||||
ErrCreateSubmissionRateLimit = fmt.Errorf("%w: You must not create more than 5 submissions every 10 minutes", ErrTooManyRequests)
|
||||
ErrDisplayNameNotUnique = fmt.Errorf("%w: Cannot submit: A map exists with the same DisplayName", ErrPermissionDenied)
|
||||
ErrCreationPhaseSubmissionsLimit = errors.New("Active submissions limited to 20")
|
||||
ErrUploadedAssetIDAlreadyExists = errors.New("The submission UploadedAssetID is already set")
|
||||
ErrReleaseInvalidStatus = errors.New("Only submissions with Uploaded status can be released")
|
||||
ErrReleaseNoUploadedAssetID = errors.New("Only submissions with a UploadedAssetID can be released")
|
||||
ErrAcceptOwnSubmission = fmt.Errorf("%w: You cannot accept your own submission as the submitter", ErrPermissionDenied)
|
||||
ErrCreateSubmissionRateLimit = errors.New("You must not create more than 5 submissions every 10 minutes")
|
||||
)
|
||||
|
||||
// POST /submissions
|
||||
func (svc *Service) CreateSubmission(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) {
|
||||
// sanitization
|
||||
if request.AssetID < 0 {
|
||||
if request.AssetID<0{
|
||||
return nil, ErrNegativeID
|
||||
}
|
||||
var ModelID = uint64(request.AssetID)
|
||||
var ModelID=uint64(request.AssetID);
|
||||
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
@@ -60,7 +60,7 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
|
||||
creation_submissions, err := svc.inner.ListSubmissions(ctx, filter, model.Page{
|
||||
Number: 1,
|
||||
Size: int32(CreationPhaseSubmissionsLimit),
|
||||
}, datastore.ListSortDisabled)
|
||||
},datastore.ListSortDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,8 +86,8 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
|
||||
}
|
||||
|
||||
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -110,14 +110,13 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
|
||||
OperationID: operation.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// POST /submissions-admin
|
||||
func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) {
|
||||
// sanitization
|
||||
if request.AssetID < 0 {
|
||||
if request.AssetID<0{
|
||||
return nil, ErrNegativeID
|
||||
}
|
||||
var ModelID = uint64(request.AssetID)
|
||||
var ModelID=uint64(request.AssetID);
|
||||
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
@@ -135,7 +134,7 @@ func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.Subm
|
||||
}
|
||||
|
||||
// check if caller has required role
|
||||
has_role := roles&model.RolesSubmissionReview == model.RolesSubmissionReview
|
||||
has_role := roles & model.RolesSubmissionReview == model.RolesSubmissionReview
|
||||
if !has_role {
|
||||
return nil, ErrPermissionDeniedNeedRoleSubmissionReview
|
||||
}
|
||||
@@ -156,8 +155,8 @@ func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.Subm
|
||||
}
|
||||
|
||||
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -192,18 +191,18 @@ func (svc *Service) GetSubmission(ctx context.Context, params api.GetSubmissionP
|
||||
return nil, err
|
||||
}
|
||||
return &api.Submission{
|
||||
ID: submission.ID,
|
||||
DisplayName: submission.DisplayName,
|
||||
Creator: submission.Creator,
|
||||
GameID: int32(submission.GameID),
|
||||
CreatedAt: submission.CreatedAt.Unix(),
|
||||
UpdatedAt: submission.UpdatedAt.Unix(),
|
||||
Submitter: int64(submission.Submitter),
|
||||
AssetID: int64(submission.AssetID),
|
||||
AssetVersion: int64(submission.AssetVersion),
|
||||
Completed: submission.Completed,
|
||||
ID: submission.ID,
|
||||
DisplayName: submission.DisplayName,
|
||||
Creator: submission.Creator,
|
||||
GameID: int32(submission.GameID),
|
||||
CreatedAt: submission.CreatedAt.Unix(),
|
||||
UpdatedAt: submission.UpdatedAt.Unix(),
|
||||
Submitter: int64(submission.Submitter),
|
||||
AssetID: int64(submission.AssetID),
|
||||
AssetVersion: int64(submission.AssetVersion),
|
||||
Completed: submission.Completed,
|
||||
UploadedAssetID: api.NewOptInt64(int64(submission.UploadedAssetID)),
|
||||
StatusID: int32(submission.StatusID),
|
||||
StatusID: int32(submission.StatusID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -215,28 +214,28 @@ func (svc *Service) GetSubmission(ctx context.Context, params api.GetSubmissionP
|
||||
func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissionsParams) (*api.Submissions, error) {
|
||||
filter := service.NewSubmissionFilter()
|
||||
|
||||
if display_name, display_name_ok := params.DisplayName.Get(); display_name_ok {
|
||||
if display_name, display_name_ok := params.DisplayName.Get(); display_name_ok{
|
||||
filter.SetDisplayName(display_name)
|
||||
}
|
||||
if creator, creator_ok := params.Creator.Get(); creator_ok {
|
||||
if creator, creator_ok := params.Creator.Get(); creator_ok{
|
||||
filter.SetCreator(creator)
|
||||
}
|
||||
if game_id, game_id_ok := params.GameID.Get(); game_id_ok {
|
||||
if game_id, game_id_ok := params.GameID.Get(); game_id_ok{
|
||||
filter.SetGameID(uint32(game_id))
|
||||
}
|
||||
if submitter, submitter_ok := params.Submitter.Get(); submitter_ok {
|
||||
if submitter, submitter_ok := params.Submitter.Get(); submitter_ok{
|
||||
filter.SetSubmitter(uint64(submitter))
|
||||
}
|
||||
if asset_id, asset_id_ok := params.AssetID.Get(); asset_id_ok {
|
||||
if asset_id, asset_id_ok := params.AssetID.Get(); asset_id_ok{
|
||||
filter.SetAssetID(uint64(asset_id))
|
||||
}
|
||||
if asset_version, asset_version_ok := params.AssetVersion.Get(); asset_version_ok {
|
||||
if asset_version, asset_version_ok := params.AssetVersion.Get(); asset_version_ok{
|
||||
filter.SetAssetVersion(uint64(asset_version))
|
||||
}
|
||||
if uploaded_asset_id, uploaded_asset_id_ok := params.UploadedAssetID.Get(); uploaded_asset_id_ok {
|
||||
if uploaded_asset_id, uploaded_asset_id_ok := params.UploadedAssetID.Get(); uploaded_asset_id_ok{
|
||||
filter.SetUploadedAssetID(uint64(uploaded_asset_id))
|
||||
}
|
||||
if status_id, status_id_ok := params.StatusID.Get(); status_id_ok {
|
||||
if status_id, status_id_ok := params.StatusID.Get(); status_id_ok{
|
||||
filter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatus(status_id)})
|
||||
}
|
||||
|
||||
@@ -245,27 +244,27 @@ func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissi
|
||||
total, items, err := svc.inner.ListSubmissionsWithTotal(ctx, filter, model.Page{
|
||||
Number: params.Page,
|
||||
Size: params.Limit,
|
||||
}, sort)
|
||||
},sort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp api.Submissions
|
||||
resp.Total = total
|
||||
resp.Total=total
|
||||
for _, item := range items {
|
||||
resp.Submissions = append(resp.Submissions, api.Submission{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: int32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: int64(item.Submitter),
|
||||
AssetID: int64(item.AssetID),
|
||||
AssetVersion: int64(item.AssetVersion),
|
||||
Completed: item.Completed,
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Creator: item.Creator,
|
||||
GameID: int32(item.GameID),
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
UpdatedAt: item.UpdatedAt.Unix(),
|
||||
Submitter: int64(item.Submitter),
|
||||
AssetID: int64(item.AssetID),
|
||||
AssetVersion: int64(item.AssetVersion),
|
||||
Completed: item.Completed,
|
||||
UploadedAssetID: api.NewOptInt64(int64(item.UploadedAssetID)),
|
||||
StatusID: int32(item.StatusID),
|
||||
StatusID: int32(item.StatusID),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -342,9 +341,9 @@ func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.Update
|
||||
}
|
||||
|
||||
event_data := model.AuditEventDataChangeModel{
|
||||
OldModelID: OldModelID,
|
||||
OldModelID: OldModelID,
|
||||
OldModelVersion: OldModelVersion,
|
||||
NewModelID: NewModelID,
|
||||
NewModelID: NewModelID,
|
||||
NewModelVersion: NewModelVersion,
|
||||
}
|
||||
|
||||
@@ -352,7 +351,7 @@ func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.Update
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -402,7 +401,7 @@ func (svc *Service) ActionSubmissionReject(ctx context.Context, params api.Actio
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -438,12 +437,7 @@ func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params a
|
||||
target_status := model.SubmissionStatusChangesRequested
|
||||
update := service.NewSubmissionUpdate()
|
||||
update.SetStatusID(target_status)
|
||||
allowed_statuses := []model.SubmissionStatus{
|
||||
model.SubmissionStatusUploaded,
|
||||
model.SubmissionStatusValidated,
|
||||
model.SubmissionStatusAcceptedUnvalidated,
|
||||
model.SubmissionStatusSubmitted,
|
||||
}
|
||||
allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusValidated, model.SubmissionStatusAcceptedUnvalidated, model.SubmissionStatusSubmitted}
|
||||
err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -457,7 +451,7 @@ func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params a
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -510,7 +504,7 @@ func (svc *Service) ActionSubmissionRevoke(ctx context.Context, params api.Actio
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -553,37 +547,11 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
|
||||
}
|
||||
}
|
||||
|
||||
// check for maps with the exact same name
|
||||
filter := service.NewMapFilter()
|
||||
filter.SetDisplayName(submission.DisplayName)
|
||||
maps_list, err := svc.inner.ListMaps(
|
||||
ctx,
|
||||
filter,
|
||||
model.Page{
|
||||
Number: 1,
|
||||
Size: 1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The map search finds substrings, we only want exact matches
|
||||
for _, m := range maps_list {
|
||||
if m.DisplayName == submission.DisplayName {
|
||||
return ErrDisplayNameNotUnique
|
||||
}
|
||||
}
|
||||
|
||||
// transaction
|
||||
target_status := model.SubmissionStatusSubmitting
|
||||
update := service.NewSubmissionUpdate()
|
||||
update.SetStatusID(target_status)
|
||||
allowed_statuses := []model.SubmissionStatus{
|
||||
model.SubmissionStatusUnderConstruction,
|
||||
model.SubmissionStatusChangesRequested,
|
||||
model.SubmissionStatusSubmitted,
|
||||
}
|
||||
allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusUnderConstruction, model.SubmissionStatusChangesRequested}
|
||||
err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -606,7 +574,7 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -677,7 +645,7 @@ func (svc *Service) ActionSubmissionTriggerSubmitUnchecked(ctx context.Context,
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -705,7 +673,7 @@ func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
|
||||
// the last time the submission was updated must be longer than 10 seconds ago
|
||||
return ErrDelayReset
|
||||
}
|
||||
@@ -734,7 +702,7 @@ func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -802,7 +770,7 @@ func (svc *Service) ActionSubmissionTriggerUpload(ctx context.Context, params ap
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -839,7 +807,7 @@ func (svc *Service) ActionSubmissionValidated(ctx context.Context, params api.Ac
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
|
||||
// the last time the submission was updated must be longer than 10 seconds ago
|
||||
return ErrDelayReset
|
||||
}
|
||||
@@ -862,7 +830,7 @@ func (svc *Service) ActionSubmissionValidated(ctx context.Context, params api.Ac
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -934,7 +902,7 @@ func (svc *Service) ActionSubmissionTriggerValidate(ctx context.Context, params
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -994,7 +962,7 @@ func (svc *Service) ActionSubmissionRetryValidate(ctx context.Context, params ap
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -1031,7 +999,7 @@ func (svc *Service) ActionSubmissionAccepted(ctx context.Context, params api.Act
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
|
||||
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
|
||||
// the last time the submission was updated must be longer than 10 seconds ago
|
||||
return ErrDelayReset
|
||||
}
|
||||
@@ -1054,7 +1022,7 @@ func (svc *Service) ActionSubmissionAccepted(ctx context.Context, params api.Act
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -1101,11 +1069,11 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
|
||||
id_to_submission := make(map[int64]*model.Submission, len(submissions))
|
||||
|
||||
// check each submission to make sure it is ready to release
|
||||
for _, submission := range submissions {
|
||||
if submission.StatusID != model.SubmissionStatusUploaded {
|
||||
for _,submission := range submissions{
|
||||
if submission.StatusID != model.SubmissionStatusUploaded{
|
||||
return nil, ErrReleaseInvalidStatus
|
||||
}
|
||||
if submission.UploadedAssetID == 0 {
|
||||
if submission.UploadedAssetID == 0{
|
||||
return nil, ErrReleaseNoUploadedAssetID
|
||||
}
|
||||
id_to_submission[submission.ID] = &submission
|
||||
@@ -1131,8 +1099,8 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
|
||||
|
||||
// create a trackable long-running operation
|
||||
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
Owner: userId,
|
||||
StatusID: model.OperationStatusCreated,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1154,10 +1122,10 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
|
||||
|
||||
// CreateSubmissionAuditComment implements createSubmissionAuditComment operation.
|
||||
//
|
||||
// # Post a comment to the audit log
|
||||
// Post a comment to the audit log
|
||||
//
|
||||
// POST /submissions/{SubmissionID}/comment
|
||||
func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.CreateSubmissionAuditCommentReq, params api.CreateSubmissionAuditCommentParams) error {
|
||||
func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.CreateSubmissionAuditCommentReq, params api.CreateSubmissionAuditCommentParams) (error) {
|
||||
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
|
||||
if !ok {
|
||||
return ErrUserInfo
|
||||
@@ -1198,7 +1166,7 @@ func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.Cr
|
||||
ctx,
|
||||
userId,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
event_data,
|
||||
@@ -1214,7 +1182,7 @@ func (svc *Service) ListSubmissionAuditEvents(ctx context.Context, params api.Li
|
||||
return svc.inner.ListAuditEvents(
|
||||
ctx,
|
||||
model.Resource{
|
||||
ID: params.SubmissionID,
|
||||
ID: params.SubmissionID,
|
||||
Type: model.ResourceSubmission,
|
||||
},
|
||||
model.Page{
|
||||
|
||||
@@ -17,7 +17,7 @@ reqwest = { version = "0", features = [
|
||||
# default features
|
||||
"charset", "http2", "system-proxy"
|
||||
], default-features = false }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_repr = "0.1.19"
|
||||
url = "2"
|
||||
|
||||
@@ -4,18 +4,18 @@ version = "0.1.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-nats.workspace = true
|
||||
futures-util.workspace = true
|
||||
rbx_asset.workspace = true
|
||||
rbx_binary.workspace = true
|
||||
rbx_dom_weak.workspace = true
|
||||
async-nats = "0.45.0"
|
||||
futures = "0.3.31"
|
||||
rbx_asset = { version = "0.5.0", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" }
|
||||
rbx_binary = "2.0.0"
|
||||
rbx_dom_weak = "4.0.0"
|
||||
rbx_reflection_database = "2.0.1"
|
||||
rbx_xml = "2.0.0"
|
||||
regex = { version = "1.11.3", default-features = false }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
siphasher = "1.0.1"
|
||||
tokio.workspace = true
|
||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||
heck = "0.5.0"
|
||||
rust-grpc = { version = "1.6.1", registry = "strafesnet" }
|
||||
tonic = "0.14.1"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
FROM debian:trixie-slim AS runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libssl3t64 ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY /target/release/maps-validation /
|
||||
FROM alpine:3.21 AS runtime
|
||||
COPY /target/x86_64-unknown-linux-musl/release/maps-validation /
|
||||
ENTRYPOINT ["/maps-validation"]
|
||||
|
||||
@@ -324,24 +324,25 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
|
||||
}
|
||||
|
||||
// check if an observed string matches an expected string
|
||||
pub struct StringEquality<'a,Str>{
|
||||
pub struct StringCheck<'a,T,Str>(Result<T,StringCheckContext<'a,Str>>);
|
||||
pub struct StringCheckContext<'a,Str>{
|
||||
observed:&'a str,
|
||||
expected:Str,
|
||||
}
|
||||
impl<'a,Str> StringEquality<'a,Str>
|
||||
impl<'a,Str> StringCheckContext<'a,Str>
|
||||
where
|
||||
&'a str:PartialEq<Str>,
|
||||
{
|
||||
/// Compute the StringCheck, passing through the provided value on success.
|
||||
fn check<T>(self,value:T)->Result<T,Self>{
|
||||
fn check<T>(self,value:T)->StringCheck<'a,T,Str>{
|
||||
if self.observed==self.expected{
|
||||
Ok(value)
|
||||
StringCheck(Ok(value))
|
||||
}else{
|
||||
Err(self)
|
||||
StringCheck(Err(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Str:std::fmt::Display> std::fmt::Display for StringEquality<'_,Str>{
|
||||
impl<Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'_,Str>{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
write!(f,"expected: {}, observed: {}",self.expected,self.observed)
|
||||
}
|
||||
@@ -463,66 +464,19 @@ impl TryFrom<MapInfo<'_>> for MapInfoOwned{
|
||||
struct Exists;
|
||||
struct Absent;
|
||||
|
||||
enum DisplayNameError<'a>{
|
||||
TitleCase(StringEquality<'a,String>),
|
||||
Empty(StringEmpty),
|
||||
TooLong(usize),
|
||||
StringValue(StringValueError),
|
||||
}
|
||||
fn check_display_name<'a>(display_name:Result<&'a str,StringValueError>)->Result<&'a str,DisplayNameError<'a>>{
|
||||
// DisplayName StringValue can be missing or whatever
|
||||
let display_name=display_name.map_err(DisplayNameError::StringValue)?;
|
||||
|
||||
// DisplayName cannot be ""
|
||||
let display_name=check_empty(display_name).map_err(DisplayNameError::Empty)?;
|
||||
|
||||
// DisplayName cannot exceed 50 characters
|
||||
if 50<display_name.len(){
|
||||
return Err(DisplayNameError::TooLong(display_name.len()));
|
||||
}
|
||||
|
||||
// Check title case
|
||||
let display_name=StringEquality{
|
||||
observed:display_name,
|
||||
expected:display_name.to_title_case(),
|
||||
}.check(display_name).map_err(DisplayNameError::TitleCase)?;
|
||||
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
enum CreatorError{
|
||||
Empty(StringEmpty),
|
||||
TooLong(usize),
|
||||
StringValue(StringValueError),
|
||||
}
|
||||
fn check_creator<'a>(creator:Result<&'a str,StringValueError>)->Result<&'a str,CreatorError>{
|
||||
// Creator StringValue can be missing or whatever
|
||||
let creator=creator.map_err(CreatorError::StringValue)?;
|
||||
|
||||
// Creator cannot be ""
|
||||
let creator=check_empty(creator).map_err(CreatorError::Empty)?;
|
||||
|
||||
// Creator cannot exceed 50 characters
|
||||
if 50<creator.len(){
|
||||
return Err(CreatorError::TooLong(creator.len()));
|
||||
}
|
||||
|
||||
Ok(creator)
|
||||
}
|
||||
|
||||
/// The result of every map check.
|
||||
struct MapCheck<'a>{
|
||||
// === METADATA CHECKS ===
|
||||
// The root must be of class Model
|
||||
model_class:Result<(),StringEquality<'a,&'static str>>,
|
||||
model_class:StringCheck<'a,(),&'static str>,
|
||||
// Model's name must be in snake case
|
||||
model_name:Result<(),StringEquality<'a,String>>,
|
||||
model_name:StringCheck<'a,(),String>,
|
||||
// Map must have a StringValue named DisplayName.
|
||||
// Value must not be empty, must be in title case.
|
||||
display_name:Result<&'a str,DisplayNameError<'a>>,
|
||||
display_name:Result<Result<StringCheck<'a,&'a str,String>,StringEmpty>,StringValueError>,
|
||||
// Map must have a StringValue named Creator.
|
||||
// Value must not be empty.
|
||||
creator:Result<&'a str,CreatorError>,
|
||||
creator:Result<Result<&'a str,StringEmpty>,StringValueError>,
|
||||
// The prefix of the model's name must match the game it was submitted for.
|
||||
// bhop_ for bhop, and surf_ for surf
|
||||
game_id:Result<GameID,ParseGameIDError>,
|
||||
@@ -557,22 +511,27 @@ struct MapCheck<'a>{
|
||||
impl<'a> ModelInfo<'a>{
|
||||
fn check(self)->MapCheck<'a>{
|
||||
// Check class is exactly "Model"
|
||||
let model_class=StringEquality{
|
||||
let model_class=StringCheckContext{
|
||||
observed:self.model_class,
|
||||
expected:"Model",
|
||||
}.check(());
|
||||
|
||||
// Check model name is snake case
|
||||
let model_name=StringEquality{
|
||||
let model_name=StringCheckContext{
|
||||
observed:self.model_name,
|
||||
expected:self.model_name.to_snake_case(),
|
||||
}.check(());
|
||||
|
||||
// Check display name is not empty and has title case
|
||||
let display_name=check_display_name(self.map_info.display_name);
|
||||
let display_name=self.map_info.display_name.map(|display_name|{
|
||||
check_empty(display_name).map(|display_name|StringCheckContext{
|
||||
observed:display_name,
|
||||
expected:display_name.to_title_case(),
|
||||
}.check(display_name))
|
||||
});
|
||||
|
||||
// Check Creator is not empty
|
||||
let creator=check_creator(self.map_info.creator);
|
||||
let creator=self.map_info.creator.map(check_empty);
|
||||
|
||||
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
||||
let game_id=self.map_info.game_id;
|
||||
@@ -671,10 +630,10 @@ impl MapCheck<'_>{
|
||||
fn result(self)->Result<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
|
||||
match self{
|
||||
MapCheck{
|
||||
model_class:Ok(()),
|
||||
model_name:Ok(()),
|
||||
display_name:Ok(display_name),
|
||||
creator:Ok(creator),
|
||||
model_class:StringCheck(Ok(())),
|
||||
model_name:StringCheck(Ok(())),
|
||||
display_name:Ok(Ok(StringCheck(Ok(display_name)))),
|
||||
creator:Ok(Ok(creator)),
|
||||
game_id:Ok(game_id),
|
||||
mapstart:Ok(Exists),
|
||||
mode_start_counts:DuplicateCheck(Ok(())),
|
||||
@@ -778,25 +737,27 @@ macro_rules! summary_format{
|
||||
impl MapCheck<'_>{
|
||||
fn itemize(&self)->Result<MapCheckList,serde_json::Error>{
|
||||
let model_class=match &self.model_class{
|
||||
Ok(())=>passed!("ModelClass"),
|
||||
Err(context)=>summary_format!("ModelClass","Invalid model class: {context}"),
|
||||
StringCheck(Ok(()))=>passed!("ModelClass"),
|
||||
StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}"),
|
||||
};
|
||||
let model_name=match &self.model_name{
|
||||
Ok(())=>passed!("ModelName"),
|
||||
Err(context)=>summary_format!("ModelName","Model name must have snake_case: {context}"),
|
||||
StringCheck(Ok(()))=>passed!("ModelName"),
|
||||
StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}"),
|
||||
};
|
||||
let display_name=match &self.display_name{
|
||||
Ok(_)=>passed!("DisplayName"),
|
||||
Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
|
||||
Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
|
||||
Err(DisplayNameError::TooLong(context))=>summary_format!("DisplayName","DisplayName is too long: {context} characters (50 characters max)"),
|
||||
Err(DisplayNameError::StringValue(context))=>summary_format!("DisplayName","DisplayName StringValue: {context}"),
|
||||
Ok(Ok(StringCheck(Ok(_))))=>passed!("DisplayName"),
|
||||
Ok(Ok(StringCheck(Err(context))))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
|
||||
Ok(Err(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
|
||||
Err(StringValueError::ObjectNotFound)=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()),
|
||||
Err(StringValueError::ValueNotSet)=>summary!("DisplayName","DisplayName Value not set".to_owned()),
|
||||
Err(StringValueError::NonStringValue)=>summary!("DisplayName","DisplayName Value is not a String".to_owned()),
|
||||
};
|
||||
let creator=match &self.creator{
|
||||
Ok(_)=>passed!("Creator"),
|
||||
Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"),
|
||||
Err(CreatorError::TooLong(context))=>summary_format!("Creator","Creator is too long: {context} characters (50 characters max)"),
|
||||
Err(CreatorError::StringValue(context))=>summary_format!("Creator","Creator StringValue: {context}"),
|
||||
Ok(Ok(_))=>passed!("Creator"),
|
||||
Ok(Err(context))=>summary_format!("Creator","Invalid Creator: {context}"),
|
||||
Err(StringValueError::ObjectNotFound)=>summary!("Creator","Missing Creator StringValue".to_owned()),
|
||||
Err(StringValueError::ValueNotSet)=>summary!("Creator","Creator Value not set".to_owned()),
|
||||
Err(StringValueError::NonStringValue)=>summary!("Creator","Creator Value is not a String".to_owned()),
|
||||
};
|
||||
let game_id=match &self.game_id{
|
||||
Ok(_)=>passed!("GameID"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures_util::StreamExt;
|
||||
use futures::StreamExt;
|
||||
|
||||
mod download;
|
||||
mod grpc;
|
||||
|
||||
@@ -79,15 +79,6 @@ pub enum StringValueError{
|
||||
ValueNotSet,
|
||||
NonStringValue,
|
||||
}
|
||||
impl std::fmt::Display for StringValueError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
match self{
|
||||
StringValueError::ObjectNotFound=>write!(f,"Missing StringValue"),
|
||||
StringValueError::ValueNotSet=>write!(f,"Value not set"),
|
||||
StringValueError::NonStringValue=>write!(f,"Value is not a String"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringValueError>{
|
||||
let instance=instance.ok_or(StringValueError::ObjectNotFound)?;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use futures_util::stream::iter as stream_iter;
|
||||
use futures_util::StreamExt;
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::download::download_asset_version;
|
||||
use crate::nats_types::ReleaseSubmissionsBatchRequest;
|
||||
@@ -93,7 +92,7 @@ async fn release_inner(
|
||||
.collect();
|
||||
|
||||
// fut_download
|
||||
let fut_download=stream_iter(asset_versions)
|
||||
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)
|
||||
@@ -138,7 +137,7 @@ async fn release_inner(
|
||||
}
|
||||
|
||||
// concurrently dispatch results
|
||||
let release_results:Vec<_> =stream_iter(
|
||||
let release_results:Vec<_> =futures::stream::iter(
|
||||
release_info
|
||||
.Submissions
|
||||
.into_iter()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use futures_util::stream::iter as stream_iter;
|
||||
use futures_util::TryStreamExt;
|
||||
use futures::TryStreamExt;
|
||||
use rust_grpc::validator::Policy;
|
||||
|
||||
use crate::download::download_asset_version;
|
||||
@@ -154,7 +153,7 @@ impl crate::message_handler::MessageHandler{
|
||||
}
|
||||
|
||||
// send all script hashes to REST endpoint and retrieve the replacements
|
||||
stream_iter(script_map.iter_mut().map(Ok))
|
||||
futures::stream::iter(script_map.iter_mut().map(Ok))
|
||||
.try_for_each_concurrent(Some(SCRIPT_CONCURRENCY),|(source,NamePolicy{policy,name})|async{
|
||||
// get the hash
|
||||
let hash=hash_source(source.as_str());
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
<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>StrafesNET | Maps</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Barlow:wght@700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background-color: #09090b; }
|
||||
</style>
|
||||
<title>Maps Service</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import { ThemeProvider, CssBaseline } from '@mui/material'
|
||||
import { ThemeProvider } from '@mui/material'
|
||||
import { theme } from '@/app/lib/theme'
|
||||
|
||||
// Pages
|
||||
@@ -22,7 +22,6 @@ import NotFound from '@/app/not-found/page'
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/maps" element={<MapsPage />} />
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { surface } from '@/app/lib/colors';
|
||||
|
||||
const AnimatedBackground: React.FC = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 0,
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
background: `
|
||||
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(124, 58, 237, 0.08) 0%, transparent 100%),
|
||||
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(34, 211, 238, 0.04) 0%, transparent 100%),
|
||||
${surface.base}
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0.03,
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: '128px 128px',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedBackground;
|
||||
@@ -75,7 +75,7 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(167, 139, 250, 0.08)',
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -93,12 +93,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid rgba(167, 139, 250, 0.15)',
|
||||
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'background.paper',
|
||||
borderColor: 'rgba(167, 139, 250, 0.3)',
|
||||
boxShadow: '0 8px 20px rgba(167, 139, 250, 0.2)',
|
||||
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||
},
|
||||
visibility: scrollPosition <= 5 ? 'hidden' : 'visible',
|
||||
}}
|
||||
@@ -146,12 +146,12 @@ export function Carousel<T extends CarouselItem>({ title, items, renderItem, vie
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid rgba(167, 139, 250, 0.15)',
|
||||
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'background.paper',
|
||||
borderColor: 'rgba(167, 139, 250, 0.3)',
|
||||
boxShadow: '0 8px 20px rgba(167, 139, 250, 0.2)',
|
||||
borderColor: 'rgba(99, 102, 241, 0.4)',
|
||||
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||
},
|
||||
visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible',
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Tab,
|
||||
keyframes
|
||||
} from "@mui/material";
|
||||
import { semantic } from "@/app/lib/colors";
|
||||
import CommentsTabPanel from './CommentsTabPanel';
|
||||
import AuditEventsTabPanel from './AuditEventsTabPanel';
|
||||
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
|
||||
@@ -76,7 +75,7 @@ export default function CommentsAndAuditSection({
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: semantic.warning,
|
||||
backgroundColor: '#ff9800',
|
||||
animation: `${pulse} 2s ease-in-out infinite`
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { primary, text, border, fill } from "@/app/lib/colors";
|
||||
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
@@ -11,44 +10,51 @@ import Box from "@mui/material/Box";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import LoginIcon from "@mui/icons-material/Login";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
|
||||
const navItems = [
|
||||
interface HeaderButton {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const navItems: HeaderButton[] = [
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Submissions", href: "/submissions" },
|
||||
{ name: "Mapfixes", href: "/mapfixes" },
|
||||
{ name: "Maps", href: "/maps" },
|
||||
];
|
||||
|
||||
const quickLinks = [
|
||||
{ name: "Bhop", href: "https://www.roblox.com/games/5315046213" },
|
||||
{ name: "Bhop Maptest", href: "https://www.roblox.com/games/517201717" },
|
||||
{ name: "Surf", href: "https://www.roblox.com/games/5315066937" },
|
||||
{ name: "Surf Maptest", href: "https://www.roblox.com/games/517206177" },
|
||||
{ name: "Fly Trials", href: "https://www.roblox.com/games/12591611759" },
|
||||
{ name: "Fly Trials Maptest", href: "https://www.roblox.com/games/12724901535" },
|
||||
];
|
||||
function HeaderButton(header: HeaderButton) {
|
||||
return (
|
||||
<Button color="inherit" component={Link} to={header.href}>
|
||||
{header.name}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
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';
|
||||
};
|
||||
|
||||
@@ -80,20 +86,9 @@ export default function Header() {
|
||||
setQuickLinksAnchor(null);
|
||||
};
|
||||
|
||||
// Mobile navigation drawer content
|
||||
const drawer = (
|
||||
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center', pt: 2 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontWeight: 700,
|
||||
color: text.primary,
|
||||
mb: 2,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
</Typography>
|
||||
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
|
||||
<List>
|
||||
{navItems.map((item) => (
|
||||
<ListItem key={item.name} disablePadding>
|
||||
@@ -105,7 +100,7 @@ export default function Header() {
|
||||
{isLoggedIn && user && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/submit" sx={{ textAlign: 'center' }}>
|
||||
<ListItemText primary="Submit Map" sx={{ color: primary.main }} />
|
||||
<ListItemText primary="Submit Map" sx={{ color: 'success.main' }} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
@@ -127,88 +122,125 @@ export default function Header() {
|
||||
</Box>
|
||||
);
|
||||
|
||||
const quickLinks = [
|
||||
{ name: "Bhop", href: "https://www.roblox.com/games/5315046213" },
|
||||
{ name: "Bhop Maptest", href: "https://www.roblox.com/games/517201717" },
|
||||
{ name: "Surf", href: "https://www.roblox.com/games/5315066937" },
|
||||
{ name: "Surf Maptest", href: "https://www.roblox.com/games/517206177" },
|
||||
{ name: "Fly Trials", href: "https://www.roblox.com/games/12591611759" },
|
||||
{ name: "Fly Trials Maptest", href: "https://www.roblox.com/games/12724901535" },
|
||||
];
|
||||
|
||||
return (
|
||||
<AppBar position="sticky">
|
||||
<Toolbar sx={{ gap: 1 }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar sx={{ py: 1 }}>
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ color: primary.main }}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* Brand */}
|
||||
{/* Desktop navigation */}
|
||||
{!isMobile && (
|
||||
<Box
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{
|
||||
mr: 4,
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
<Box display="flex" flexGrow={1} gap={1} alignItems="center">
|
||||
{/* Logo/Brand */}
|
||||
<Box
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.02em',
|
||||
color: text.primary,
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
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,
|
||||
},
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: text.muted,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
Maps
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<Box
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{ textDecoration: 'none', display: 'flex', alignItems: 'baseline', gap: 0.75 }}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontWeight: 700,
|
||||
color: text.primary,
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
fontSize: '1rem',
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 500, color: text.muted, userSelect: 'none' }}>
|
||||
Maps
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Desktop nav items */}
|
||||
{!isMobile && (
|
||||
<>
|
||||
<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) => (
|
||||
<Button
|
||||
key={item.name}
|
||||
@@ -216,157 +248,192 @@ export default function Header() {
|
||||
component={Link}
|
||||
to={item.href}
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
borderRadius: 2,
|
||||
fontSize: '0.85rem',
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderRadius: 1.5,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
color: text.tertiary,
|
||||
transition: 'all 0.15s ease',
|
||||
color: 'text.secondary',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
color: text.primary,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
{/* Quick Links Dropdown */}
|
||||
{!isMobile && (
|
||||
<Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleQuickLinksOpen}
|
||||
sx={{
|
||||
px: 1.5,
|
||||
borderRadius: 2,
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 500,
|
||||
color: text.dim,
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
color: text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Quick Links
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={quickLinksAnchor}
|
||||
open={Boolean(quickLinksAnchor)}
|
||||
onClose={handleQuickLinksClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
sx={{ '& .MuiMenu-paper': { mt: 1 } }}
|
||||
>
|
||||
{quickLinks.map(link => (
|
||||
<MenuItem
|
||||
key={link.name}
|
||||
onClick={handleQuickLinksClose}
|
||||
sx={{ minWidth: 200, fontSize: '0.85rem' }}
|
||||
component="a"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Submit + Auth */}
|
||||
{!isMobile && isLoggedIn && user && (
|
||||
<Button
|
||||
variant="contained"
|
||||
component={Link}
|
||||
to="/submit"
|
||||
size="small"
|
||||
sx={{ px: 2 }}
|
||||
>
|
||||
Submit Map
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isLoggedIn && user ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
{/* Quick Links Dropdown */}
|
||||
<Box>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleQuickLinksOpen}
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
p: 0.5,
|
||||
transition: 'all 0.15s ease',
|
||||
px: 2,
|
||||
mr: 1,
|
||||
borderRadius: 1.5,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500,
|
||||
color: 'text.secondary',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
boxShadow: '0 0 12px rgba(167, 139, 250, 0.3)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={user.AvatarURL}
|
||||
>
|
||||
Quick Links
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={quickLinksAnchor}
|
||||
open={Boolean(quickLinksAnchor)}
|
||||
onClose={handleQuickLinksClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700,
|
||||
backgroundColor: primary.dark,
|
||||
color: text.primary,
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{user.Username?.slice(0, 2).toUpperCase()}
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
sx={{ '& .MuiMenu-paper': { mt: 1 } }}
|
||||
>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={getAuthUrl()}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
Manage Account
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
{quickLinks.map(link => (
|
||||
<MenuItem
|
||||
key={link.name}
|
||||
onClick={handleQuickLinksClose}
|
||||
sx={{
|
||||
minWidth: 200,
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
component="a"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleLoginClick}
|
||||
sx={{
|
||||
ml: 1,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
minWidth: 0,
|
||||
borderRadius: 2,
|
||||
backgroundColor: fill.default,
|
||||
border: `1px solid ${border.default}`,
|
||||
color: text.dim,
|
||||
fontSize: '0.8rem',
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.hover,
|
||||
borderColor: border.primaryMedium,
|
||||
color: primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LoginIcon sx={{ fontSize: 16, mr: 0.5 }} />
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Spacer for mobile view */}
|
||||
{isMobile && <Box sx={{ flexGrow: 1 }} />}
|
||||
|
||||
{/* Right side of nav */}
|
||||
<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 && isLoggedIn && user ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Button
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
size="small"
|
||||
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)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className="avatar"
|
||||
width={28}
|
||||
height={28}
|
||||
src={user.AvatarURL}
|
||||
alt={user.Username}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.875rem', fontWeight: 500 }}>
|
||||
{user.Username}
|
||||
</Typography>
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiMenu-paper': {
|
||||
mt: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={getAuthUrl()}
|
||||
sx={{
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
Manage Account
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
) : !isMobile && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleLoginClick}
|
||||
sx={{
|
||||
px: 3,
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* In mobile view, display just the avatar if logged in */}
|
||||
{isMobile && isLoggedIn && user && (
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
<img
|
||||
className="avatar"
|
||||
width={32}
|
||||
height={32}
|
||||
src={user.AvatarURL}
|
||||
alt={user.Username}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
@@ -374,7 +441,9 @@ export default function Header() {
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
@@ -386,4 +455,4 @@ export default function Header() {
|
||||
</Drawer>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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";
|
||||
import { primary, gameColors } from "@/app/lib/colors";
|
||||
|
||||
interface MapCardProps {
|
||||
displayName: string;
|
||||
@@ -120,13 +119,13 @@ export function MapCard(props: MapCardProps) {
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Explore sx={{ fontSize: '1rem', color: gameColors[props.gameID] || primary.main }} />
|
||||
<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: primary.main }} />
|
||||
<Person2 sx={{ fontSize: '1rem', color: '#8b5cf6' }} />
|
||||
{props.type === 'mapfix' && usernameLoading ? (
|
||||
<Skeleton variant="text" width={80} />
|
||||
) : (
|
||||
|
||||
@@ -29,13 +29,6 @@ const ReviewActions = {
|
||||
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,
|
||||
Update: {
|
||||
name: "Update Model",
|
||||
action: "trigger-submit",
|
||||
confirmTitle: "Re-submit Latest Version",
|
||||
confirmMessage: "This action is equivalent to clicking Revoke and then clicking Submit.",
|
||||
requiresConfirmation: true
|
||||
} as ReviewAction,
|
||||
AdminSubmit: {
|
||||
name: "Submit on Behalf of User",
|
||||
action: "trigger-submit",
|
||||
@@ -190,13 +183,6 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (status === Status.Submitted) {
|
||||
submitterButtons.push({
|
||||
action: ReviewActions.Update,
|
||||
color: "success",
|
||||
});
|
||||
}
|
||||
|
||||
if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) {
|
||||
submitterButtons.push({
|
||||
action: ReviewActions.Revoke,
|
||||
@@ -243,7 +229,7 @@ const ReviewButtons: React.FC<ReviewButtonsProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (StatusMatches(status, [Status.Uploaded, Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||
if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) {
|
||||
reviewerButtons.push({
|
||||
action: ReviewActions.RequestChanges,
|
||||
color: "warning",
|
||||
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
import { semantic } from "@/app/lib/colors";
|
||||
|
||||
const pulse = keyframes`
|
||||
0%, 100% {
|
||||
@@ -189,8 +188,8 @@ const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type,
|
||||
icon: InfoOutlinedIcon,
|
||||
title: 'Not Yet Submitted',
|
||||
message: 'Your submission has been created but has not been submitted. Click "Submit" to submit it.',
|
||||
color: semantic.info,
|
||||
bgColor: 'rgba(56, 189, 248, 0.08)'
|
||||
color: '#2196f3',
|
||||
bgColor: 'rgba(33, 150, 243, 0.08)'
|
||||
};
|
||||
}
|
||||
if (isChangesRequested) {
|
||||
@@ -198,8 +197,8 @@ const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type,
|
||||
icon: WarningIcon,
|
||||
title: 'Changes Requested',
|
||||
message: 'Review comments and audit events, make modifications, and submit again.',
|
||||
color: semantic.warning,
|
||||
bgColor: 'rgba(251, 191, 36, 0.08)'
|
||||
color: '#ff9800',
|
||||
bgColor: 'rgba(255, 152, 0, 0.08)'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,76 +1,86 @@
|
||||
import {JSX} from "react";
|
||||
import {Cancel, CheckCircle, Pending} from "@mui/icons-material";
|
||||
import {Chip} from "@mui/material";
|
||||
import { semantic, text } from "@/app/lib/colors";
|
||||
|
||||
interface StatusConfig {
|
||||
bg: string;
|
||||
color: string;
|
||||
border: string;
|
||||
icon: JSX.Element;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function getStatusConfig(status: number): StatusConfig {
|
||||
const warn = {
|
||||
bg: `rgba(251, 191, 36, 0.08)`,
|
||||
color: semantic.warning,
|
||||
border: `rgba(251, 191, 36, 0.2)`,
|
||||
};
|
||||
const info = {
|
||||
bg: `rgba(56, 189, 248, 0.08)`,
|
||||
color: semantic.info,
|
||||
border: `rgba(56, 189, 248, 0.2)`,
|
||||
};
|
||||
const success = {
|
||||
bg: `rgba(74, 222, 128, 0.08)`,
|
||||
color: semantic.success,
|
||||
border: `rgba(74, 222, 128, 0.2)`,
|
||||
};
|
||||
const error = {
|
||||
bg: `rgba(248, 113, 113, 0.08)`,
|
||||
color: semantic.error,
|
||||
border: `rgba(248, 113, 113, 0.2)`,
|
||||
};
|
||||
const gray = {
|
||||
bg: `rgba(161, 161, 170, 0.08)`,
|
||||
color: text.tertiary,
|
||||
border: `rgba(161, 161, 170, 0.2)`,
|
||||
};
|
||||
|
||||
switch (status) {
|
||||
case 0: return { ...warn, icon: <Pending fontSize="small" />, label: 'Under Construction' };
|
||||
case 1: return { ...warn, icon: <Pending fontSize="small" />, label: 'Changes Requested' };
|
||||
case 2: return { ...info, icon: <Pending fontSize="small" />, label: 'Submitting' };
|
||||
case 3: return { ...warn, icon: <CheckCircle fontSize="small" />, label: 'Under Review' };
|
||||
case 4: return { ...warn, icon: <Pending fontSize="small" />, label: 'Script Review' };
|
||||
case 5: return { ...info, icon: <Pending fontSize="small" />, label: 'Validating' };
|
||||
case 6: return { ...success, icon: <CheckCircle fontSize="small" />, label: 'Validated' };
|
||||
case 7: return { ...info, icon: <Pending fontSize="small" />, label: 'Uploading' };
|
||||
case 8: return { ...success, icon: <CheckCircle fontSize="small" />, label: 'Uploaded' };
|
||||
case 9: return { ...error, icon: <Cancel fontSize="small" />, label: 'Rejected' };
|
||||
case 10: return { ...success, icon: <CheckCircle fontSize="small" />, label: 'Released' };
|
||||
case 11: return { ...info, icon: <Pending fontSize="small" />, label: 'Releasing' };
|
||||
default: return { ...gray, icon: <Pending fontSize="small" />, label: 'Unknown' };
|
||||
}
|
||||
}
|
||||
|
||||
export const StatusChip = ({status}: { status: number }): JSX.Element => {
|
||||
const config = getStatusConfig(status);
|
||||
let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
|
||||
let icon: JSX.Element = <Pending fontSize="small"/>;
|
||||
let label: string = 'Unknown';
|
||||
|
||||
switch (status) {
|
||||
case 0:
|
||||
color = 'warning';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Under Construction';
|
||||
break;
|
||||
case 1:
|
||||
color = 'warning';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Changes Requested';
|
||||
break;
|
||||
case 2:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Submitting';
|
||||
break;
|
||||
case 3:
|
||||
color = 'warning';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Under Review';
|
||||
break;
|
||||
case 4:
|
||||
color = 'warning';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Script Review';
|
||||
break;
|
||||
case 5:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Validating';
|
||||
break;
|
||||
case 6:
|
||||
color = 'success';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Validated';
|
||||
break;
|
||||
case 7:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Uploading';
|
||||
break;
|
||||
case 8:
|
||||
color = 'success';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Uploaded';
|
||||
break;
|
||||
case 11:
|
||||
color = 'info';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Releasing';
|
||||
break;
|
||||
case 9:
|
||||
color = 'error';
|
||||
icon = <Cancel fontSize="small"/>;
|
||||
label = 'Rejected';
|
||||
break;
|
||||
case 10:
|
||||
color = 'success';
|
||||
icon = <CheckCircle fontSize="small"/>;
|
||||
label = 'Released';
|
||||
break;
|
||||
default:
|
||||
color = 'default';
|
||||
icon = <Pending fontSize="small"/>;
|
||||
label = 'Unknown';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
icon={config.icon}
|
||||
label={config.label}
|
||||
icon={icon}
|
||||
label={label}
|
||||
color={color}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: config.bg,
|
||||
color: config.color,
|
||||
border: `1px solid ${config.border}`,
|
||||
'& .MuiChip-icon': {
|
||||
color: config.color,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { Box } from "@mui/material";
|
||||
import Header from "./header";
|
||||
import AnimatedBackground from "./AnimatedBackground";
|
||||
|
||||
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {
|
||||
return <>
|
||||
<AnimatedBackground />
|
||||
<Box sx={{ position: 'relative', zIndex: 1, minHeight: '100vh' }}>
|
||||
<Header/>
|
||||
{children}
|
||||
</Box>
|
||||
<Header/>
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
color: var(--text-color);
|
||||
}
|
||||
& fieldset {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgb(100,100,100);
|
||||
}
|
||||
& span {
|
||||
color: #fafafa;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
$review-border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
$review-border: 1px solid var(--review-border);
|
||||
$form-label-fontsize: 1.3rem;
|
||||
|
||||
@mixin border-with-radius {
|
||||
border: $review-border {
|
||||
radius: 8px;
|
||||
radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
--review-border: rgba(255, 255, 255, 0.06);
|
||||
--text-color: #fafafa;
|
||||
--placeholder-text: #52525b;
|
||||
--header-height: 45px;
|
||||
|
||||
--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);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
background-color: var(--page);
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -22,3 +41,10 @@ button {
|
||||
a:active, a:link, a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--review-border);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Brand / accent
|
||||
export const primary = {
|
||||
main: '#a78bfa',
|
||||
light: '#c4b5fd',
|
||||
dark: '#7c3aed',
|
||||
darker: '#6d28d9',
|
||||
mid: '#8b5cf6',
|
||||
} as const;
|
||||
|
||||
export const secondary = {
|
||||
main: '#22d3ee',
|
||||
light: '#67e8f9',
|
||||
dark: '#0891b2',
|
||||
} as const;
|
||||
|
||||
// Semantic
|
||||
export const semantic = {
|
||||
error: '#f87171',
|
||||
warning: '#fbbf24',
|
||||
success: '#4ade80',
|
||||
info: '#38bdf8',
|
||||
} as const;
|
||||
|
||||
// Surfaces
|
||||
export const surface = {
|
||||
base: '#09090b',
|
||||
raised: '#18181b',
|
||||
raisedAlpha: 'rgba(24, 24, 27, 0.6)',
|
||||
raisedSolid: 'rgba(24, 24, 27, 0.95)',
|
||||
overlay: 'rgba(0, 0, 0, 0.6)',
|
||||
appBar: 'rgba(9, 9, 11, 0.8)',
|
||||
} as const;
|
||||
|
||||
// Text hierarchy (lightest -> dimmest)
|
||||
export const text = {
|
||||
primary: '#fafafa',
|
||||
secondary: '#d4d4d8',
|
||||
tertiary: '#a1a1aa',
|
||||
muted: '#71717a',
|
||||
dim: '#52525b',
|
||||
faint: '#3f3f46',
|
||||
} as const;
|
||||
|
||||
// Borders & dividers
|
||||
export const border = {
|
||||
subtle: 'rgba(255, 255, 255, 0.04)',
|
||||
default: 'rgba(255, 255, 255, 0.06)',
|
||||
medium: 'rgba(255, 255, 255, 0.08)',
|
||||
strong: 'rgba(255, 255, 255, 0.1)',
|
||||
primarySubtle: 'rgba(167, 139, 250, 0.1)',
|
||||
primaryDefault: 'rgba(167, 139, 250, 0.15)',
|
||||
primaryMedium: 'rgba(167, 139, 250, 0.2)',
|
||||
primaryStrong: 'rgba(167, 139, 250, 0.3)',
|
||||
} as const;
|
||||
|
||||
// Interactive surface fills
|
||||
export const fill = {
|
||||
subtle: 'rgba(255, 255, 255, 0.03)',
|
||||
default: 'rgba(255, 255, 255, 0.04)',
|
||||
hover: 'rgba(255, 255, 255, 0.06)',
|
||||
primaryHover: 'rgba(167, 139, 250, 0.06)',
|
||||
primaryActive: 'rgba(167, 139, 250, 0.08)',
|
||||
primaryStrong: 'rgba(167, 139, 250, 0.1)',
|
||||
} as const;
|
||||
|
||||
// Gradient presets
|
||||
export const gradients = {
|
||||
brand: `linear-gradient(135deg, ${primary.dark}, ${secondary.main})`,
|
||||
brandText: `linear-gradient(135deg, ${primary.light} 0%, ${secondary.main} 100%)`,
|
||||
button: `linear-gradient(135deg, ${primary.dark} 0%, ${primary.main} 100%)`,
|
||||
buttonHover: `linear-gradient(135deg, ${primary.darker} 0%, ${primary.mid} 100%)`,
|
||||
titleText: `linear-gradient(135deg, ${text.primary} 0%, ${text.tertiary} 100%)`,
|
||||
} as const;
|
||||
|
||||
// Glow / shadow presets
|
||||
export const glow = {
|
||||
brand: '0 0 12px rgba(124, 58, 237, 0.5)',
|
||||
brandStrong: '0 0 20px rgba(124, 58, 237, 0.8), 0 0 40px rgba(34, 211, 238, 0.2)',
|
||||
button: '0 0 20px rgba(124, 58, 237, 0.3)',
|
||||
palette: '0 24px 80px rgba(0, 0, 0, 0.5), 0 0 60px rgba(124, 58, 237, 0.1)',
|
||||
} as const;
|
||||
|
||||
// Game colors
|
||||
export const gameColors: Record<number, string> = {
|
||||
1: '#a78bfa', // Bhop - purple
|
||||
2: '#22d3ee', // Surf - cyan
|
||||
5: '#fbbf24', // Fly Trials - yellow
|
||||
} as const;
|
||||
@@ -1,263 +1,133 @@
|
||||
import { createTheme } from '@mui/material';
|
||||
import { primary, secondary, semantic, surface, text, border, fill, gradients, glow } from './colors';
|
||||
import {createTheme} from "@mui/material";
|
||||
|
||||
export const theme = createTheme({
|
||||
cssVariables: {
|
||||
colorSchemeSelector: 'class',
|
||||
},
|
||||
colorSchemes: {
|
||||
dark: true,
|
||||
},
|
||||
defaultColorScheme: 'dark',
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: primary.main,
|
||||
light: primary.light,
|
||||
dark: primary.dark,
|
||||
main: '#3b82f6',
|
||||
dark: '#2563eb',
|
||||
light: '#60a5fa',
|
||||
},
|
||||
secondary: {
|
||||
main: secondary.main,
|
||||
light: secondary.light,
|
||||
dark: secondary.dark,
|
||||
main: '#8b5cf6',
|
||||
dark: '#7c3aed',
|
||||
light: '#a78bfa',
|
||||
},
|
||||
background: {
|
||||
default: surface.base,
|
||||
paper: surface.raised,
|
||||
default: '#0a0a0a',
|
||||
paper: '#171717',
|
||||
},
|
||||
error: { main: semantic.error },
|
||||
warning: { main: semantic.warning },
|
||||
success: { main: semantic.success },
|
||||
info: { main: semantic.info },
|
||||
text: {
|
||||
primary: text.primary,
|
||||
secondary: text.tertiary,
|
||||
primary: '#ffffff',
|
||||
secondary: '#9ca3af',
|
||||
},
|
||||
error: {
|
||||
main: '#ef4444',
|
||||
light: '#f87171',
|
||||
dark: '#dc2626',
|
||||
},
|
||||
warning: {
|
||||
main: '#f59e0b',
|
||||
light: '#fbbf24',
|
||||
dark: '#d97706',
|
||||
},
|
||||
success: {
|
||||
main: '#10b981',
|
||||
light: '#34d399',
|
||||
dark: '#059669',
|
||||
},
|
||||
info: {
|
||||
main: '#3b82f6',
|
||||
light: '#60a5fa',
|
||||
dark: '#2563eb',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif',
|
||||
h1: {
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.025em',
|
||||
},
|
||||
h2: {
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.02em',
|
||||
},
|
||||
h3: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.015em',
|
||||
},
|
||||
h4: {
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.01em',
|
||||
},
|
||||
h5: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
subtitle1: {
|
||||
fontWeight: 500,
|
||||
fontSize: '1rem',
|
||||
},
|
||||
body1: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1.7,
|
||||
},
|
||||
body2: {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.6,
|
||||
},
|
||||
caption: {
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
button: {
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
letterSpacing: '0.01em',
|
||||
},
|
||||
divider: border.default,
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 12,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontSize: 14,
|
||||
h1: { fontWeight: 700, letterSpacing: '-0.025em' },
|
||||
h2: { fontWeight: 700, letterSpacing: '-0.02em' },
|
||||
h3: { fontWeight: 600, letterSpacing: '-0.015em' },
|
||||
h4: { fontWeight: 700, letterSpacing: '-0.02em' },
|
||||
h5: { fontWeight: 700, letterSpacing: '-0.02em' },
|
||||
h6: { fontWeight: 700, letterSpacing: '-0.01em' },
|
||||
body1: { fontSize: '1rem', lineHeight: 1.7 },
|
||||
body2: { fontSize: '0.875rem', lineHeight: 1.6 },
|
||||
button: { fontWeight: 600, textTransform: 'none' as const },
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
backgroundColor: surface.base,
|
||||
backgroundImage: 'radial-gradient(ellipse 80% 50% at 50% -20%, rgba(120, 60, 255, 0.15), transparent)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: surface.appBar,
|
||||
backdropFilter: 'blur(16px)',
|
||||
boxShadow: 'none',
|
||||
borderBottom: `1px solid ${border.default}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
backgroundColor: surface.raisedAlpha,
|
||||
backdropFilter: 'blur(12px)',
|
||||
border: `1px solid ${border.default}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: surface.raisedAlpha,
|
||||
backdropFilter: 'blur(12px)',
|
||||
border: `1px solid ${border.default}`,
|
||||
boxShadow: 'none',
|
||||
backgroundColor: '#171717',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
border: `1px solid ${border.primaryMedium}`,
|
||||
boxShadow: glow.button,
|
||||
border: '1px solid rgba(59, 130, 246, 0.4)',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(59, 130, 246, 0.2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardMedia: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: 'transform 0.3s',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 16,
|
||||
'&:last-child': { paddingBottom: 16 },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableContainer: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
borderRadius: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTableCell-head': {
|
||||
backgroundColor: fill.subtle,
|
||||
color: text.tertiary,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: '0.05em',
|
||||
borderBottom: `1px solid ${border.default}`,
|
||||
padding: '12px 16px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableBody: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTableRow-root': {
|
||||
transition: 'background-color 0.15s ease',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
},
|
||||
},
|
||||
'& .MuiTableCell-body': {
|
||||
borderBottom: `1px solid ${border.subtle}`,
|
||||
padding: '14px 16px',
|
||||
fontSize: '0.875rem',
|
||||
color: text.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none' as const,
|
||||
fontWeight: 500,
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 44,
|
||||
'&.Mui-selected': { color: primary.main },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
styleOverrides: {
|
||||
indicator: {
|
||||
backgroundColor: primary.main,
|
||||
height: 2,
|
||||
borderRadius: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none' as const,
|
||||
fontWeight: 600,
|
||||
borderRadius: 8,
|
||||
},
|
||||
contained: {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
containedPrimary: {
|
||||
background: gradients.button,
|
||||
'&:hover': {
|
||||
boxShadow: glow.button,
|
||||
background: gradients.buttonHover,
|
||||
},
|
||||
},
|
||||
containedSuccess: {
|
||||
backgroundColor: semantic.success,
|
||||
color: '#000',
|
||||
'&:hover': {
|
||||
backgroundColor: '#22c55e',
|
||||
boxShadow: '0 0 16px rgba(74, 222, 128, 0.3)',
|
||||
},
|
||||
},
|
||||
containedError: {
|
||||
backgroundColor: semantic.error,
|
||||
'&:hover': {
|
||||
backgroundColor: '#ef4444',
|
||||
boxShadow: '0 0 16px rgba(248, 113, 113, 0.3)',
|
||||
},
|
||||
},
|
||||
containedWarning: {
|
||||
backgroundColor: semantic.warning,
|
||||
color: '#000',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f59e0b',
|
||||
boxShadow: '0 0 16px rgba(251, 191, 36, 0.3)',
|
||||
},
|
||||
},
|
||||
containedInfo: {
|
||||
backgroundColor: semantic.info,
|
||||
'&:hover': {
|
||||
backgroundColor: '#0ea5e9',
|
||||
boxShadow: '0 0 16px rgba(56, 189, 248, 0.3)',
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
borderColor: border.primaryStrong,
|
||||
color: primary.main,
|
||||
'&:hover': {
|
||||
borderColor: primary.main,
|
||||
backgroundColor: fill.primaryActive,
|
||||
},
|
||||
},
|
||||
outlinedSuccess: {
|
||||
borderColor: `rgba(74, 222, 128, 0.4)`,
|
||||
color: semantic.success,
|
||||
'&:hover': {
|
||||
borderColor: semantic.success,
|
||||
backgroundColor: 'rgba(74, 222, 128, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedError: {
|
||||
borderColor: `rgba(248, 113, 113, 0.4)`,
|
||||
color: semantic.error,
|
||||
'&:hover': {
|
||||
borderColor: semantic.error,
|
||||
backgroundColor: 'rgba(248, 113, 113, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedWarning: {
|
||||
borderColor: `rgba(251, 191, 36, 0.4)`,
|
||||
color: semantic.warning,
|
||||
'&:hover': {
|
||||
borderColor: semantic.warning,
|
||||
backgroundColor: 'rgba(251, 191, 36, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedInfo: {
|
||||
borderColor: `rgba(56, 189, 248, 0.4)`,
|
||||
color: semantic.info,
|
||||
'&:hover': {
|
||||
borderColor: semantic.info,
|
||||
backgroundColor: 'rgba(56, 189, 248, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: 8,
|
||||
'& fieldset': { borderColor: border.strong },
|
||||
'&:hover fieldset': { borderColor: border.primaryStrong },
|
||||
'&.Mui-focused fieldset': { borderColor: primary.main },
|
||||
'&:last-child': {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -266,39 +136,134 @@ export const theme = createTheme({
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
borderRadius: 6,
|
||||
fontSize: '0.75rem',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
},
|
||||
icon: {
|
||||
marginLeft: '8px',
|
||||
},
|
||||
colorError: {
|
||||
backgroundColor: '#ef4444',
|
||||
color: '#ffffff',
|
||||
'& .MuiChip-icon': {
|
||||
color: '#ffffff',
|
||||
},
|
||||
},
|
||||
colorWarning: {
|
||||
backgroundColor: '#f59e0b',
|
||||
color: '#ffffff',
|
||||
'& .MuiChip-icon': {
|
||||
color: '#ffffff',
|
||||
},
|
||||
},
|
||||
colorSuccess: {
|
||||
backgroundColor: '#10b981',
|
||||
color: '#ffffff',
|
||||
'& .MuiChip-icon': {
|
||||
color: '#ffffff',
|
||||
},
|
||||
},
|
||||
colorInfo: {
|
||||
backgroundColor: '#3b82f6',
|
||||
color: '#ffffff',
|
||||
'& .MuiChip-icon': {
|
||||
color: '#ffffff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDivider: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderColor: border.default,
|
||||
borderColor: 'rgba(148, 163, 184, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
backgroundColor: '#171717',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
fontWeight: 600,
|
||||
textTransform: 'none',
|
||||
padding: '10px 24px',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
},
|
||||
contained: {
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
},
|
||||
containedPrimary: {
|
||||
background: '#3b82f6',
|
||||
'&:hover': {
|
||||
background: '#2563eb',
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
borderWidth: '1.5px',
|
||||
'&:hover': {
|
||||
borderWidth: '1.5px',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedPrimary: {
|
||||
borderColor: 'rgba(59, 130, 246, 0.5)',
|
||||
'&:hover': {
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.08)',
|
||||
},
|
||||
},
|
||||
outlinedSecondary: {
|
||||
borderColor: 'rgba(139, 92, 246, 0.5)',
|
||||
'&:hover': {
|
||||
borderColor: '#8b5cf6',
|
||||
backgroundColor: 'rgba(139, 92, 246, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
background: 'rgba(10, 10, 10, 0.8)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDrawer: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
backgroundColor: surface.base,
|
||||
borderRight: `1px solid ${border.default}`,
|
||||
backgroundColor: '#0a0a0a',
|
||||
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCircularProgress: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: primary.main,
|
||||
color: '#3b82f6',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: 'all 0.15s ease',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryActive,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -306,11 +271,11 @@ export const theme = createTheme({
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: primary.light,
|
||||
color: '#60a5fa',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 0.15s ease',
|
||||
transition: 'color 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
color: primary.main,
|
||||
color: '#3b82f6',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
@@ -319,28 +284,28 @@ export const theme = createTheme({
|
||||
MuiMenu: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
background: surface.raisedSolid,
|
||||
backdropFilter: 'blur(16px)',
|
||||
border: `1px solid ${border.default}`,
|
||||
boxShadow: glow.palette,
|
||||
background: '#171717',
|
||||
backdropFilter: 'blur(12px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: 'all 0.15s ease',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryHover,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: fill.primaryActive,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.15)',
|
||||
'&:hover': {
|
||||
backgroundColor: fill.primaryStrong,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -391,12 +391,12 @@ export default function MapDetails() {
|
||||
px: 2,
|
||||
borderRadius: 1,
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: 'rgba(167, 139, 250, 0.08)',
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.08)',
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'primary.main',
|
||||
mb: releasedFixes.length > 0 ? 2 : 0,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(167, 139, 250, 0.12)',
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.12)',
|
||||
transform: 'translateX(4px)'
|
||||
},
|
||||
textDecoration: 'none',
|
||||
|
||||
@@ -4,7 +4,6 @@ import Webpage from "@/app/_components/webpage";
|
||||
import { useTitle } from "@/app/hooks/useTitle";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import MapIcon from "@mui/icons-material/Map";
|
||||
import { semantic, surface } from "@/app/lib/colors";
|
||||
|
||||
export default function NotFound() {
|
||||
useTitle("404 - Page Not Found");
|
||||
@@ -20,7 +19,7 @@ export default function NotFound() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
background: `linear-gradient(to bottom, ${surface.base} 0%, #0f0f0f 100%)`,
|
||||
background: 'linear-gradient(to bottom, #0a0a0a 0%, #0f0f0f 100%)',
|
||||
}}
|
||||
>
|
||||
{/* Subtle Gradient Background */}
|
||||
@@ -31,7 +30,7 @@ export default function NotFound() {
|
||||
right: '30%',
|
||||
width: '500px',
|
||||
height: '500px',
|
||||
background: `radial-gradient(circle, rgba(248, 113, 113, 0.1) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
}}
|
||||
@@ -43,7 +42,7 @@ export default function NotFound() {
|
||||
left: '25%',
|
||||
width: '450px',
|
||||
height: '450px',
|
||||
background: `radial-gradient(circle, rgba(167, 139, 250, 0.08) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
}}
|
||||
@@ -60,7 +59,7 @@ export default function NotFound() {
|
||||
lineHeight: 1,
|
||||
mb: 2,
|
||||
letterSpacing: '-0.04em',
|
||||
background: `linear-gradient(135deg, ${semantic.error} 0%, #dc2626 100%)`,
|
||||
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
@@ -174,7 +173,7 @@ export default function NotFound() {
|
||||
fontSize: '1rem',
|
||||
'&:hover': {
|
||||
color: 'primary.main',
|
||||
background: `rgba(167, 139, 250, 0.1)`,
|
||||
background: 'rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,6 @@ import RocketLaunchIcon from "@mui/icons-material/RocketLaunch";
|
||||
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { hasAnyReviewerRole } from "@/app/ts/Roles";
|
||||
import { primary, secondary, semantic, text, border, fill, gradients, glow } from "@/app/lib/colors";
|
||||
|
||||
export default function Home() {
|
||||
useTitle("Home");
|
||||
@@ -168,8 +167,8 @@ export default function Home() {
|
||||
}}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
|
||||
<CircularProgress size={48} thickness={3} />
|
||||
<Typography variant="body2" sx={{ color: text.muted }}>
|
||||
<CircularProgress size={60} thickness={4} sx={{ color: 'primary.main' }}/>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Loading content...
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -224,32 +223,32 @@ export default function Home() {
|
||||
value: totalSubmissions,
|
||||
label: 'Total Submissions',
|
||||
sublabel: 'Total maps submitted by the community',
|
||||
color: primary.main,
|
||||
gradient: gradients.button,
|
||||
color: '#3b82f6',
|
||||
gradient: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
||||
},
|
||||
{
|
||||
icon: <BuildIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
value: totalMapfixes,
|
||||
label: 'Total Map Fixes',
|
||||
sublabel: 'Total map fixes submitted by the community',
|
||||
color: secondary.main,
|
||||
gradient: `linear-gradient(135deg, ${secondary.dark} 0%, ${secondary.main} 100%)`,
|
||||
color: '#8b5cf6',
|
||||
gradient: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
|
||||
},
|
||||
{
|
||||
icon: <EmojiEventsIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
value: releasedSubmissions + releasedMapfixes,
|
||||
label: 'Total Released',
|
||||
sublabel: 'Maps & fixes that have been released to the game',
|
||||
color: semantic.success,
|
||||
gradient: `linear-gradient(135deg, ${semantic.success} 0%, #059669 100%)`,
|
||||
color: '#10b981',
|
||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
},
|
||||
{
|
||||
icon: <EmojiEventsIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
value: releasedSubmissions,
|
||||
label: 'Released Submissions',
|
||||
sublabel: 'Approved maps that have been published to the game',
|
||||
color: semantic.success,
|
||||
gradient: `linear-gradient(135deg, ${semantic.success} 0%, #059669 100%)`,
|
||||
color: '#10b981',
|
||||
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
},
|
||||
{
|
||||
icon: <EmojiEventsIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
|
||||
@@ -264,8 +263,8 @@ export default function Home() {
|
||||
value: submittedSubmissions + submittedMapfixes,
|
||||
label: 'Under Review',
|
||||
sublabel: 'Pending approval fixes & submissions',
|
||||
color: semantic.warning,
|
||||
gradient: `linear-gradient(135deg, ${semantic.warning} 0%, #d97706 100%)`,
|
||||
color: '#f59e0b',
|
||||
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -285,8 +284,8 @@ export default function Home() {
|
||||
}}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
|
||||
<CircularProgress size={48} thickness={3} />
|
||||
<Typography variant="body2" sx={{ color: text.muted }}>
|
||||
<CircularProgress size={60} thickness={4} sx={{ color: 'primary.main' }}/>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -306,7 +305,7 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<Webpage>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ width: '100%', bgcolor: 'background.default' }}>
|
||||
{/* Hero Section */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -315,9 +314,10 @@ export default function Home() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
background: 'radial-gradient(ellipse at top, #0f1419 0%, #0a0a0a 50%, #000000 100%)',
|
||||
}}
|
||||
>
|
||||
{/* Animated Background Orbs */}
|
||||
{/* Animated Background Elements */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
@@ -325,7 +325,7 @@ export default function Home() {
|
||||
right: '15%',
|
||||
width: { xs: '400px', md: '600px' },
|
||||
height: { xs: '400px', md: '600px' },
|
||||
background: `radial-gradient(circle, rgba(124, 58, 237, 0.12) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.15) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
animation: 'float 25s ease-in-out infinite',
|
||||
@@ -342,11 +342,11 @@ export default function Home() {
|
||||
left: '10%',
|
||||
width: { xs: '350px', md: '500px' },
|
||||
height: { xs: '350px', md: '500px' },
|
||||
background: `radial-gradient(circle, rgba(34, 211, 238, 0.08) 0%, transparent 70%)`,
|
||||
background: 'radial-gradient(circle, rgba(139, 92, 246, 0.12) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(80px)',
|
||||
animation: 'floatReverse 30s ease-in-out infinite',
|
||||
'@keyframes floatReverse': {
|
||||
animation: 'float-reverse 30s ease-in-out infinite',
|
||||
'@keyframes float-reverse': {
|
||||
'0%, 100%': { transform: 'translate(0, 0) scale(1)' },
|
||||
'50%': { transform: 'translate(-30px, 30px) scale(1.15)' },
|
||||
},
|
||||
@@ -359,8 +359,8 @@ export default function Home() {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(124, 58, 237, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(124, 58, 237, 0.03) 1px, transparent 1px)
|
||||
linear-gradient(rgba(59, 130, 246, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(59, 130, 246, 0.03) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '60px 60px',
|
||||
maskImage: 'radial-gradient(ellipse at center, black 20%, transparent 70%)',
|
||||
@@ -376,7 +376,7 @@ export default function Home() {
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '1px',
|
||||
background: `linear-gradient(90deg, transparent 0%, rgba(124, 58, 237, 0.3) 50%, transparent 100%)`,
|
||||
background: 'linear-gradient(90deg, transparent 0%, rgba(59, 130, 246, 0.3) 50%, transparent 100%)',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
@@ -387,7 +387,7 @@ export default function Home() {
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '1px',
|
||||
background: `linear-gradient(90deg, transparent 0%, rgba(34, 211, 238, 0.2) 50%, transparent 100%)`,
|
||||
background: 'linear-gradient(90deg, transparent 0%, rgba(139, 92, 246, 0.3) 50%, transparent 100%)',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
@@ -412,7 +412,7 @@ export default function Home() {
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.2em',
|
||||
textTransform: 'uppercase',
|
||||
color: primary.main,
|
||||
color: 'primary.main',
|
||||
mb: 3,
|
||||
display: 'block',
|
||||
opacity: 0.9,
|
||||
@@ -424,16 +424,16 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
fontFamily: '"Barlow", sans-serif',
|
||||
fontSize: { xs: '3.5rem', sm: '5rem', md: '7rem', lg: '8rem' },
|
||||
fontWeight: 900,
|
||||
lineHeight: 0.95,
|
||||
mb: 2,
|
||||
letterSpacing: '-0.04em',
|
||||
background: gradients.brandText,
|
||||
background: 'linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #c084fc 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
textShadow: '0 0 80px rgba(59, 130, 246, 0.3)',
|
||||
}}
|
||||
>
|
||||
StrafesNET
|
||||
@@ -442,7 +442,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{
|
||||
color: text.primary,
|
||||
color: 'text.primary',
|
||||
fontSize: { xs: '1.75rem', sm: '2.25rem', md: '3rem' },
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.02em',
|
||||
@@ -456,7 +456,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
color: text.tertiary,
|
||||
color: 'text.secondary',
|
||||
mb: 5,
|
||||
lineHeight: 1.75,
|
||||
fontWeight: 400,
|
||||
@@ -471,7 +471,7 @@ export default function Home() {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
{/* CTA Buttons - Moved up for better hierarchy */}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={3}
|
||||
@@ -493,10 +493,13 @@ export default function Home() {
|
||||
px: { xs: 4, md: 5 },
|
||||
py: { xs: 1.75, md: 2.25 },
|
||||
fontWeight: 700,
|
||||
boxShadow: glow.button,
|
||||
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
||||
boxShadow: '0 8px 32px rgba(59, 130, 246, 0.4)',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
boxShadow: glow.brandStrong,
|
||||
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
|
||||
boxShadow: '0 12px 40px rgba(59, 130, 246, 0.6)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
@@ -516,9 +519,14 @@ export default function Home() {
|
||||
py: { xs: 1.75, md: 2.25 },
|
||||
fontWeight: 700,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(139, 92, 246, 0.5)',
|
||||
color: '#a78bfa',
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
borderWidth: 2,
|
||||
borderColor: '#a78bfa',
|
||||
background: 'rgba(139, 92, 246, 0.1)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
@@ -528,7 +536,7 @@ export default function Home() {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Stats Section */}
|
||||
{/* Stats Section - Completely Redesigned */}
|
||||
<Box
|
||||
sx={{
|
||||
animation: 'fadeIn 1.1s ease-out 0.4s both',
|
||||
@@ -538,6 +546,7 @@ export default function Home() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Stats Grid */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
@@ -566,13 +575,22 @@ export default function Home() {
|
||||
cursor: 'pointer',
|
||||
background: currentStatIndex === index
|
||||
? `linear-gradient(135deg, ${stat.color}15 0%, ${stat.color}08 100%)`
|
||||
: fill.subtle,
|
||||
: 'rgba(17, 17, 17, 0.4)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: currentStatIndex === index
|
||||
? `1px solid ${stat.color}40`
|
||||
: `1px solid ${border.subtle}`,
|
||||
: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 3,
|
||||
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&::before': currentStatIndex === index ? {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: -1,
|
||||
background: stat.gradient,
|
||||
borderRadius: 3,
|
||||
opacity: 0.1,
|
||||
zIndex: -1,
|
||||
} : {},
|
||||
'&:hover': {
|
||||
transform: 'translateY(-8px) scale(1.02)',
|
||||
background: `linear-gradient(135deg, ${stat.color}20 0%, ${stat.color}10 100%)`,
|
||||
@@ -581,6 +599,7 @@ export default function Home() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -604,12 +623,13 @@ export default function Home() {
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* Value */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 900,
|
||||
fontSize: { xs: '1.75rem', md: '2.25rem' },
|
||||
color: currentStatIndex === index ? stat.color : text.primary,
|
||||
color: currentStatIndex === index ? stat.color : 'text.primary',
|
||||
letterSpacing: '-0.03em',
|
||||
transition: 'color 0.3s',
|
||||
lineHeight: 1,
|
||||
@@ -618,10 +638,11 @@ export default function Home() {
|
||||
{stat.value}
|
||||
</Typography>
|
||||
|
||||
{/* Label */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: currentStatIndex === index ? text.primary : text.tertiary,
|
||||
color: currentStatIndex === index ? 'text.primary' : 'text.secondary',
|
||||
fontSize: { xs: '0.7rem', md: '0.75rem' },
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
@@ -638,6 +659,7 @@ export default function Home() {
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Featured Stat Description */}
|
||||
<Box
|
||||
key={currentStatIndex}
|
||||
sx={{
|
||||
@@ -651,7 +673,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: text.muted,
|
||||
color: 'text.secondary',
|
||||
fontSize: { xs: '0.9rem', md: '1rem' },
|
||||
fontWeight: 500,
|
||||
maxWidth: '600px',
|
||||
@@ -686,7 +708,8 @@ export default function Home() {
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: text.muted, maxWidth: '600px' }}
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: '600px' }}
|
||||
>
|
||||
Discover the newest custom maps created by the community
|
||||
</Typography>
|
||||
@@ -717,7 +740,8 @@ export default function Home() {
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: text.muted, maxWidth: '600px' }}
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: '600px' }}
|
||||
>
|
||||
Community-created map fixes and improvements
|
||||
</Typography>
|
||||
@@ -747,7 +771,8 @@ export default function Home() {
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: text.muted, maxWidth: '600px' }}
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: '600px' }}
|
||||
>
|
||||
Join the community and start contributing today
|
||||
</Typography>
|
||||
@@ -766,21 +791,21 @@ export default function Home() {
|
||||
title: 'Submit Maps',
|
||||
description: 'Upload your custom bhop and surf maps for review. Maps are evaluated by moderators before being added to the game.',
|
||||
link: '/submit',
|
||||
color: primary.main,
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
icon: <BuildIcon sx={{ fontSize: 48 }} />,
|
||||
title: 'Submit Fixes',
|
||||
description: 'Found bugs or issues in existing maps? Submit fixed versions to improve map quality for all players.',
|
||||
link: '/mapfixes',
|
||||
color: secondary.main,
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
icon: <ListIcon sx={{ fontSize: 48 }} />,
|
||||
title: 'View Submissions',
|
||||
description: 'Browse all pending and approved submissions currently in the review queue. Track submission status and feedback.',
|
||||
link: '/submissions',
|
||||
color: semantic.success,
|
||||
color: '#10b981',
|
||||
},
|
||||
].map((card, index) => (
|
||||
<Box
|
||||
@@ -789,17 +814,16 @@ export default function Home() {
|
||||
to={card.link}
|
||||
sx={{
|
||||
p: 5,
|
||||
background: fill.subtle,
|
||||
backdropFilter: 'blur(12px)',
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${border.default}`,
|
||||
background: 'rgba(23, 23, 23, 0.5)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
borderColor: `${card.color}40`,
|
||||
boxShadow: `0 8px 24px rgba(0, 0, 0, 0.4)`,
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4)',
|
||||
'& .icon-box': {
|
||||
background: `${card.color}30`,
|
||||
},
|
||||
@@ -811,8 +835,8 @@ export default function Home() {
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
background: `${card.color}15`,
|
||||
borderRadius: 1.5,
|
||||
background: `${card.color}20`,
|
||||
mb: 3,
|
||||
color: card.color,
|
||||
transition: 'background 0.3s',
|
||||
@@ -823,7 +847,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontWeight: 600,
|
||||
mb: 1.5,
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
@@ -833,7 +857,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: text.muted,
|
||||
color: 'text.secondary',
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
@@ -850,7 +874,8 @@ export default function Home() {
|
||||
sx={{
|
||||
position: 'relative',
|
||||
py: 12,
|
||||
borderTop: `1px solid ${border.default}`,
|
||||
background: '#0f0f0f',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md" sx={{ position: 'relative', textAlign: 'center' }}>
|
||||
@@ -868,7 +893,7 @@ export default function Home() {
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: text.muted,
|
||||
color: 'text.secondary',
|
||||
mb: 5,
|
||||
lineHeight: 1.7,
|
||||
fontSize: '1.125rem',
|
||||
|
||||
@@ -174,7 +174,7 @@ export default function ReviewerDashboardPage() {
|
||||
const [scriptPoliciesCount, setScriptPoliciesCount] = useState<number>(0);
|
||||
const [isLoadingScripts, setIsLoadingScripts] = useState(false);
|
||||
|
||||
// Fetch user roles
|
||||
// Fetch user roles
|
||||
useEffect(() => {
|
||||
// Fetch roles from API
|
||||
const controller = new AbortController();
|
||||
@@ -459,9 +459,6 @@ export default function ReviewerDashboardPage() {
|
||||
);
|
||||
const canReviewScripts = hasRole(userRoles, RolesConstants.ScriptWrite);
|
||||
|
||||
const tabIndexSubmissions = 0;
|
||||
const tabIndexMapfixes = canReviewSubmissions ? 1 : 0;
|
||||
|
||||
if (!hasAnyReviewerRole(userRoles)) {
|
||||
return (
|
||||
<Webpage>
|
||||
@@ -522,10 +519,7 @@ export default function ReviewerDashboardPage() {
|
||||
mb: 4
|
||||
}}>
|
||||
{canReviewSubmissions && (
|
||||
<Card
|
||||
onClick={() => setTabValue(tabIndexSubmissions)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<AssignmentIcon sx={{ fontSize: 40, color: 'primary.main' }} />
|
||||
@@ -549,10 +543,7 @@ export default function ReviewerDashboardPage() {
|
||||
)}
|
||||
|
||||
{canReviewMapfixes && (
|
||||
<Card
|
||||
onClick={() => setTabValue(tabIndexMapfixes)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<BuildIcon sx={{ fontSize: 40, color: 'secondary.main' }} />
|
||||
@@ -579,7 +570,15 @@ export default function ReviewerDashboardPage() {
|
||||
<Card
|
||||
component={Link}
|
||||
to="/script-review"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 4
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
@@ -625,8 +624,8 @@ export default function ReviewerDashboardPage() {
|
||||
})()}
|
||||
</Box>
|
||||
}
|
||||
id={`reviewer-tab-${tabIndexSubmissions}`}
|
||||
aria-controls={`reviewer-tabpanel-${tabIndexSubmissions}`}
|
||||
id="reviewer-tab-0"
|
||||
aria-controls="reviewer-tabpanel-0"
|
||||
/>
|
||||
)}
|
||||
{canReviewMapfixes && (
|
||||
@@ -646,8 +645,8 @@ export default function ReviewerDashboardPage() {
|
||||
})()}
|
||||
</Box>
|
||||
}
|
||||
id={`reviewer-tab-${tabIndexMapfixes}`}
|
||||
aria-controls={`reviewer-tabpanel-${tabIndexMapfixes}`}
|
||||
id={`reviewer-tab-${canReviewSubmissions ? 1 : 0}`}
|
||||
aria-controls={`reviewer-tabpanel-${canReviewSubmissions ? 1 : 0}`}
|
||||
/>
|
||||
)}
|
||||
</Tabs>
|
||||
@@ -655,7 +654,7 @@ export default function ReviewerDashboardPage() {
|
||||
|
||||
{/* Submissions Tab */}
|
||||
{canReviewSubmissions && (
|
||||
<TabPanel value={tabValue} index={tabIndexSubmissions}>
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
{userRoles && submissions && groupSubmissionsByStatus(submissions.Submissions, userRoles).reduce((sum, group) => sum + group.items.length, 0) === 0 ? (
|
||||
<Alert severity="success">
|
||||
No submissions currently need your review. Great job!
|
||||
@@ -741,7 +740,7 @@ export default function ReviewerDashboardPage() {
|
||||
|
||||
{/* Map Fixes Tab */}
|
||||
{canReviewMapfixes && (
|
||||
<TabPanel value={tabValue} index={tabIndexMapfixes}>
|
||||
<TabPanel value={tabValue} index={canReviewSubmissions ? 1 : 0}>
|
||||
{userRoles && mapfixes && groupMapfixesByStatus(mapfixes.Mapfixes, userRoles).reduce((sum, group) => sum + group.items.length, 0) === 0 ? (
|
||||
<Alert severity="success">
|
||||
No map fixes currently need your review. Great job!
|
||||
|
||||
@@ -34,7 +34,6 @@ import { Script } from "@/app/ts/Script";
|
||||
import { useTitle } from "@/app/hooks/useTitle";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { RolesConstants, hasRole } from "@/app/ts/Roles";
|
||||
import { primary, semantic, surface, text, border } from "@/app/lib/colors";
|
||||
|
||||
interface SnackbarState {
|
||||
open: boolean;
|
||||
@@ -62,43 +61,43 @@ const IDEButton = ({
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return {
|
||||
bg: primary.dark,
|
||||
hoverBg: primary.mid,
|
||||
activeBg: primary.main,
|
||||
bg: '#0e639c',
|
||||
hoverBg: '#1177bb',
|
||||
activeBg: '#007acc',
|
||||
color: '#ffffff',
|
||||
border: primary.main,
|
||||
border: '#007acc',
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
bg: '#166534',
|
||||
hoverBg: '#15803d',
|
||||
activeBg: semantic.success,
|
||||
bg: '#0e7e0e',
|
||||
hoverBg: '#0f9d0f',
|
||||
activeBg: '#14b814',
|
||||
color: '#ffffff',
|
||||
border: semantic.success,
|
||||
border: '#14b814',
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
bg: '#7f1d1d',
|
||||
hoverBg: '#991b1b',
|
||||
activeBg: semantic.error,
|
||||
bg: '#7e0e0e',
|
||||
hoverBg: '#9d0f0f',
|
||||
activeBg: '#b81414',
|
||||
color: '#ffffff',
|
||||
border: semantic.error,
|
||||
border: '#b81414',
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
bg: '#78350f',
|
||||
hoverBg: '#92400e',
|
||||
activeBg: semantic.warning,
|
||||
bg: '#7e5e0e',
|
||||
hoverBg: '#9d750f',
|
||||
activeBg: '#b88614',
|
||||
color: '#ffffff',
|
||||
border: semantic.warning,
|
||||
border: '#b88614',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bg: 'transparent',
|
||||
hoverBg: 'rgba(255, 255, 255, 0.08)',
|
||||
activeBg: 'rgba(255, 255, 255, 0.12)',
|
||||
color: text.secondary,
|
||||
border: text.dim,
|
||||
color: '#cccccc',
|
||||
border: '#3e3e42',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -164,13 +163,13 @@ const InfoBadge = ({
|
||||
const getColors = () => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return { bg: `rgba(251, 191, 36, 0.15)`, border: semantic.warning, color: semantic.warning };
|
||||
return { bg: 'rgba(250, 200, 90, 0.15)', border: '#fac85a', color: '#fac85a' };
|
||||
case 'error':
|
||||
return { bg: `rgba(248, 113, 113, 0.15)`, border: semantic.error, color: semantic.error };
|
||||
return { bg: 'rgba(240, 82, 82, 0.15)', border: '#f05252', color: '#f05252' };
|
||||
case 'success':
|
||||
return { bg: `rgba(74, 222, 128, 0.15)`, border: semantic.success, color: semantic.success };
|
||||
return { bg: 'rgba(80, 200, 120, 0.15)', border: '#50c878', color: '#50c878' };
|
||||
default:
|
||||
return { bg: `rgba(167, 139, 250, 0.15)`, border: primary.main, color: primary.main };
|
||||
return { bg: 'rgba(100, 150, 230, 0.15)', border: '#6496e6', color: '#6496e6' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -637,15 +636,15 @@ export default function ScriptReviewPage() {
|
||||
<Box sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bgcolor: surface.raised,
|
||||
bgcolor: '#1e1e1e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: text.secondary,
|
||||
color: '#cccccc',
|
||||
}}>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<LinearProgress sx={{ mb: 2, width: 300 }} />
|
||||
<Typography sx={{ color: text.secondary, fontSize: '14px' }}>Loading script...</Typography>
|
||||
<Typography sx={{ color: '#cccccc', fontSize: '14px' }}>Loading script...</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -704,13 +703,13 @@ export default function ScriptReviewPage() {
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
bgcolor: surface.raised,
|
||||
bgcolor: '#1e1e1e',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Title Bar */}
|
||||
<Box sx={{
|
||||
bgcolor: '#27272a',
|
||||
borderBottom: `1px solid ${border.default}`,
|
||||
bgcolor: '#323233',
|
||||
borderBottom: '1px solid #2b2b2c',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: 2,
|
||||
@@ -718,8 +717,8 @@ export default function ScriptReviewPage() {
|
||||
gap: 2,
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<CodeIcon sx={{ fontSize: 20, color: primary.dark }} />
|
||||
<Typography sx={{ fontSize: '13px', fontWeight: 600, color: text.secondary, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
|
||||
<CodeIcon sx={{ fontSize: 20, color: '#007acc' }} />
|
||||
<Typography sx={{ fontSize: '13px', fontWeight: 600, color: '#cccccc', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
|
||||
Script Review
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -733,7 +732,7 @@ export default function ScriptReviewPage() {
|
||||
>
|
||||
Previous
|
||||
</IDEButton>
|
||||
<Typography sx={{ fontSize: '12px', color: text.muted, px: 1 }}>
|
||||
<Typography sx={{ fontSize: '12px', color: '#858585', px: 1 }}>
|
||||
{currentIndex + 1} / {allScripts.length}
|
||||
</Typography>
|
||||
<IDEButton
|
||||
@@ -753,12 +752,12 @@ export default function ScriptReviewPage() {
|
||||
gap: 1,
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
bgcolor: `rgba(251, 191, 36, 0.15)`,
|
||||
bgcolor: 'rgba(250, 200, 90, 0.15)',
|
||||
borderRadius: '2px',
|
||||
border: `1px solid ${semantic.warning}`,
|
||||
border: '1px solid #fac85a',
|
||||
}}>
|
||||
<WarningAmberIcon sx={{ fontSize: '14px', color: semantic.warning }} />
|
||||
<Typography sx={{ fontSize: '11px', color: semantic.warning, fontWeight: 500 }}>
|
||||
<WarningAmberIcon sx={{ fontSize: '14px', color: '#fac85a' }} />
|
||||
<Typography sx={{ fontSize: '11px', color: '#fac85a', fontWeight: 500 }}>
|
||||
UNSAVED CHANGES
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -788,16 +787,16 @@ export default function ScriptReviewPage() {
|
||||
width: 300,
|
||||
minWidth: 300,
|
||||
flexShrink: 0,
|
||||
bgcolor: surface.raised,
|
||||
borderRight: `1px solid ${border.default}`,
|
||||
bgcolor: '#252526',
|
||||
borderRight: '1px solid #2b2b2c',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
{/* Script Info Section */}
|
||||
<Box sx={{ p: 2, borderBottom: `1px solid ${border.default}` }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid #2b2b2c' }}>
|
||||
<Typography sx={{
|
||||
color: text.muted,
|
||||
color: '#858585',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.5px',
|
||||
@@ -807,11 +806,11 @@ export default function ScriptReviewPage() {
|
||||
SCRIPT PROPERTIES
|
||||
</Typography>
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<Typography sx={{ color: text.muted, fontSize: '10px', mb: 0.5 }}>
|
||||
<Typography sx={{ color: '#858585', fontSize: '10px', mb: 0.5 }}>
|
||||
Name
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
color: text.secondary,
|
||||
color: '#d4d4d4',
|
||||
fontFamily: '"Cascadia Code", "Courier New", monospace',
|
||||
fontSize: '12px',
|
||||
wordBreak: 'break-word',
|
||||
@@ -820,11 +819,11 @@ export default function ScriptReviewPage() {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography sx={{ color: text.muted, fontSize: '10px', mb: 0.5 }}>
|
||||
<Typography sx={{ color: '#858585', fontSize: '10px', mb: 0.5 }}>
|
||||
Hash
|
||||
</Typography>
|
||||
<Typography sx={{
|
||||
color: text.muted,
|
||||
color: '#858585',
|
||||
fontFamily: '"Cascadia Code", "Courier New", monospace',
|
||||
fontSize: '10px',
|
||||
wordBreak: 'break-all',
|
||||
@@ -835,9 +834,9 @@ export default function ScriptReviewPage() {
|
||||
</Box>
|
||||
|
||||
{/* Policy Selection Section */}
|
||||
<Box sx={{ p: 2, borderBottom: `1px solid ${border.default}` }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid #2b2b2c' }}>
|
||||
<Typography sx={{
|
||||
color: text.muted,
|
||||
color: '#858585',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.5px',
|
||||
@@ -930,9 +929,9 @@ export default function ScriptReviewPage() {
|
||||
title="Purge Script & Policy"
|
||||
style={{
|
||||
padding: '6px',
|
||||
backgroundColor: submitting || sourceChanged ? text.dim : '#7f1d1d',
|
||||
backgroundColor: submitting || sourceChanged ? '#3e3e42' : '#7e0e0e',
|
||||
color: '#ffffff',
|
||||
border: `1px solid ${submitting || sourceChanged ? text.dim : semantic.error}`,
|
||||
border: `1px solid ${submitting || sourceChanged ? '#3e3e42' : '#b81414'}`,
|
||||
borderRadius: '2px',
|
||||
cursor: submitting || sourceChanged ? 'not-allowed' : 'pointer',
|
||||
opacity: submitting || sourceChanged ? 0.4 : 1,
|
||||
@@ -947,22 +946,22 @@ export default function ScriptReviewPage() {
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = '#991b1b';
|
||||
e.currentTarget.style.backgroundColor = '#9d0f0f';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = '#7f1d1d';
|
||||
e.currentTarget.style.backgroundColor = '#7e0e0e';
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = semantic.error;
|
||||
e.currentTarget.style.backgroundColor = '#b81414';
|
||||
}
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
if (!submitting && !sourceChanged) {
|
||||
e.currentTarget.style.backgroundColor = '#991b1b';
|
||||
e.currentTarget.style.backgroundColor = '#9d0f0f';
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -977,15 +976,15 @@ export default function ScriptReviewPage() {
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Box sx={{
|
||||
p: 1.5,
|
||||
bgcolor: `rgba(248, 113, 113, 0.15)`,
|
||||
border: `1px solid ${semantic.error}`,
|
||||
bgcolor: 'rgba(240, 82, 82, 0.15)',
|
||||
border: '1px solid #f05252',
|
||||
borderRadius: '2px',
|
||||
mb: 1,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '11px', color: semantic.error, fontWeight: 500, mb: 0.5 }}>
|
||||
<Typography sx={{ fontSize: '11px', color: '#f05252', fontWeight: 500, mb: 0.5 }}>
|
||||
⚠️ Permanent Deletion
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '10px', color: text.secondary, lineHeight: 1.4 }}>
|
||||
<Typography sx={{ fontSize: '10px', color: '#cccccc', lineHeight: 1.4 }}>
|
||||
This will permanently delete the script and policy. This action cannot be undone.
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -1022,21 +1021,21 @@ export default function ScriptReviewPage() {
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Tab Bar */}
|
||||
<Box sx={{
|
||||
bgcolor: '#27272a',
|
||||
borderBottom: `1px solid ${surface.raised}`,
|
||||
bgcolor: '#2d2d2d',
|
||||
borderBottom: '1px solid #1e1e1e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
minHeight: 35,
|
||||
}}>
|
||||
<Box sx={{
|
||||
bgcolor: surface.raised,
|
||||
bgcolor: '#1e1e1e',
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
borderTop: sourceChanged ? `2px solid ${semantic.warning}` : `2px solid ${primary.dark}`,
|
||||
borderTop: sourceChanged ? '2px solid #f59e0b' : '2px solid #007acc',
|
||||
color: '#ffffff',
|
||||
minHeight: 35,
|
||||
}}>
|
||||
@@ -1049,7 +1048,7 @@ export default function ScriptReviewPage() {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: semantic.warning
|
||||
bgcolor: '#f59e0b'
|
||||
}} />
|
||||
)}
|
||||
</Box>
|
||||
@@ -1114,7 +1113,7 @@ export default function ScriptReviewPage() {
|
||||
|
||||
{/* Status Bar */}
|
||||
<Box sx={{
|
||||
bgcolor: primary.dark,
|
||||
bgcolor: '#007acc',
|
||||
color: 'white',
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
@@ -1153,9 +1152,9 @@ export default function ScriptReviewPage() {
|
||||
onClose={handleCloseSnackbar}
|
||||
severity={snackbar.severity}
|
||||
sx={{
|
||||
bgcolor: surface.raised,
|
||||
color: text.secondary,
|
||||
border: `1px solid ${text.faint}`,
|
||||
bgcolor: '#252526',
|
||||
color: '#cccccc',
|
||||
border: '1px solid #3e3e42',
|
||||
}}
|
||||
>
|
||||
{snackbar.message}
|
||||
|
||||
@@ -6,7 +6,6 @@ import Webpage from "@/app/_components/webpage";
|
||||
import { ListSortConstants } from "../ts/Sort";
|
||||
import { hasAnyReviewerRole } from "../ts/Roles";
|
||||
import { useUser } from "@/app/hooks/useUser";
|
||||
import { primary, semantic } from "@/app/lib/colors";
|
||||
import {
|
||||
Box,
|
||||
Breadcrumbs,
|
||||
@@ -408,7 +407,7 @@ export default function UserDashboardPage() {
|
||||
gap: 2,
|
||||
mb: 4
|
||||
}}>
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${primary.main} 0%, ${primary.dark} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
Total Contributions
|
||||
@@ -423,7 +422,7 @@ export default function UserDashboardPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${semantic.success} 0%, ${primary.mid} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
Released
|
||||
@@ -438,7 +437,7 @@ export default function UserDashboardPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${semantic.info} 0%, ${primary.light} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
In Review
|
||||
@@ -453,7 +452,7 @@ export default function UserDashboardPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card sx={{ background: `linear-gradient(135deg, ${semantic.error} 0%, ${semantic.warning} 100%)` }}>
|
||||
<Card sx={{ background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }}>
|
||||
<CardContent>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
|
||||
Action Needed
|
||||
|
||||
@@ -16,17 +16,17 @@ export function getGameInfo(gameId: number) {
|
||||
case 1:
|
||||
return {
|
||||
name: "Bhop",
|
||||
color: "#a78bfa" // purple
|
||||
color: "#2196f3" // blue
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
name: "Surf",
|
||||
color: "#22d3ee" // cyan
|
||||
color: "#4caf50" // green
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
name: "Fly Trials",
|
||||
color: "#fbbf24" // yellow
|
||||
color: "#ff9800" // orange
|
||||
};
|
||||
default:
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user