7 Commits

Author SHA1 Message Date
b31ba5d278 Fix kustomize
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-28 10:57:30 -05:00
29cbab545f Remove ns
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-28 10:55:26 -05:00
60ef6a5df6 Manual pg setup
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-28 10:53:32 -05:00
1688178cd3 Remove kustomize from deps
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-28 10:51:45 -05:00
ab5f1289c4 Add kind setup script
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-28 10:47:43 -05:00
2c4627d467 Add skeleton aor logic
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-28 04:37:01 -05:00
34017ee771 Add review endpoint
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-28 03:40:29 -05:00
99 changed files with 8679 additions and 7072 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"] }

View File

@@ -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

View File

@@ -26,6 +26,7 @@ func main() {
app.Commands = []*cli.Command{
cmds.NewServeCommand(),
cmds.NewApiCommand(),
cmds.NewAORCommand(),
}
if err := app.Run(os.Args); err != nil {

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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)
}
}

View File

@@ -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(())
}

View File

@@ -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,
}

View File

@@ -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
}
}

View File

@@ -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")
}
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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
View 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 "$@"

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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"
)

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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"`

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
View 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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View 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
}

View 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
}

View File

@@ -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 {

View File

@@ -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}
}

View File

@@ -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
}

View 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
View 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
}

View File

@@ -91,7 +91,3 @@ type ReleaseMapfixRequest struct {
ModelVersion uint64
TargetAssetID uint64
}
type SeedCombobulatorRequest struct {
AssetID uint64
}

View 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
}

View File

@@ -1,9 +1,8 @@
package dto
import (
"time"
"git.itzana.me/strafesnet/go-grpc/maps_extended"
"time"
)
type MapFilter struct {

View File

@@ -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

View File

@@ -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
View 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)
}

View 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
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View 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
View 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
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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,

View 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
}

View File

@@ -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{

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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"),

View File

@@ -1,4 +1,4 @@
use futures_util::StreamExt;
use futures::StreamExt;
mod download;
mod grpc;

View File

@@ -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)?;

View File

@@ -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()

View File

@@ -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());

View File

@@ -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>

View File

@@ -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 />} />

View File

@@ -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;

View File

@@ -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',
}}

View File

@@ -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`
}}
/>

View File

@@ -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>
);
}
}

View File

@@ -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} />
) : (

View File

@@ -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",

View File

@@ -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;

View File

@@ -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,
},
}}
/>
);
};

View File

@@ -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}
</>
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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)',
},
},
},
},
},
},
});
});

View File

@@ -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',

View File

@@ -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)',
},
}}
>

View File

@@ -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',

View File

@@ -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!

View File

@@ -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}

View File

@@ -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

View File

@@ -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 {