51 Commits

Author SHA1 Message Date
a5711cdc30 backend: test for exact DisplayName match
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
- The ListMaps method is unexpectedly finding DisplayName substrings
2026-03-24 19:39:27 -07:00
4a399cc2ea format
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-24 19:37:53 -07:00
f46bdfc87f Merge pull request 'Tweak Reviewer Cards' (#345) from review-cards into staging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #345
Reviewed-by: itzaname <itzaname@noreply@itzana.me>
2026-03-08 00:18:12 +00:00
d7823a82c0 web: fix script review card style
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-07 16:14:01 -08:00
ad8be22b87 web: use pointer for review cards 2026-03-07 16:13:47 -08:00
2f2c51be36 Merge pull request 'Update style to match other StrafesNET sites' (#343) from feature/style-update into staging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #343
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
2026-03-07 23:54:58 +00:00
9f952d7e54 Update style to match other sites
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-07 18:35:51 -05:00
d26126c9d3 combobulator: use up to 16 parallel requests
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-06 10:07:26 -08:00
b6ac6ce47f validator: switch futures to leaner futures-util 2026-03-06 09:50:12 -08:00
4e8ebd826a combobulator: skip 403
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-06 07:39:28 -08:00
0005a55ae0 combobulator: don't give up for conversion errors
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-03-05 09:45:30 -08:00
4e3048e272 Add Combobulate Endpoint (#338)
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
Adds the ability to seed a single map for combobulation.

Reviewed-on: #338
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-03-05 17:12:05 +00:00
5549a123a2 update deps, notable rbx_loader
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-05 08:57:33 -08:00
77d43e1e25 combobulator: skip 404
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-04 16:26:45 -08:00
6d9fb5bca6 update deps, notably rbx_loader
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-04 09:30:20 -08:00
2118a8ab35 Add string search for display_name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-03 20:55:05 -05:00
277cd819c2 Categorize Errors to avoid HTTP 500 (#326)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
- Depends on #325 (lazyness)
- Closes #148

No guarantees we won't see 500s, but I tried ok

Reviewed-on: #326
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-03-03 17:51:58 +00:00
c22717831d Check for maps with the exact same name on submit (#325)
All checks were successful
continuous-integration/drone/push Build is passing
Closes #273.
Could be better but meh.

Reviewed-on: #325
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-03-03 17:49:08 +00:00
ed8a54370c Limit DisplayName and Creator to 50 characters (#323)
Some checks failed
continuous-integration/drone/push Build is failing
Closes #276

Reviewed-on: #323
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-03-03 17:48:10 +00:00
7756bbb06d update deps, notably rbx_loader & map_tool
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-03 09:38:20 -08:00
3b8da9a8a3 list workspace dependencies 2026-03-03 09:33:58 -08:00
46290834c3 combobulator: save a clone in cold path
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-02 17:28:20 -08:00
ec4e0cf6fa combobulator: use cached assets
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-03-02 17:19:38 -08:00
3dff802bb1 update roblox_emulator to fix infinite luau loops
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2026-03-02 16:54:59 -08:00
a6ff551bee combobulator: skip "Asset is not approved for the requester"
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-02 16:24:20 -08:00
dbd28ea87b Rework Combobulator Texture Loading (#329)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
rbx_loader was attempting to load textures and other assets from disk.  Rework the system to stop implicitly loading from disk.

Reviewed-on: #329
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-03-02 23:59:40 +00:00
0efb07b52a Merge remote-tracking branch 'origin/master' into staging
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-02 10:24:12 -08:00
d4e50c2d37 Add SNFM download endpoints and batch seed endpoint (#328)
All checks were successful
continuous-integration/drone/push Build is passing
Adds download endpoint to RPC and Public API
Adds bulk seed endpoint: POST /v1/maps-admin/seed-combobulator

Reviewed-on: #328
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2026-03-02 02:39:41 +00:00
501b0933e6 Remove unused struct
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2026-03-01 20:11:22 -05:00
078a3e4c4a Add map seed endpoint
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-01 17:41:11 -05:00
02873e82b6 Just use normal asset download
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-01 17:10:53 -05:00
0e2ffcd570 Doc updates
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-01 16:39:58 -05:00
c788344bf3 Add snfm download endpoints
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-01 16:28:37 -05:00
0711774153 Merge pull request 'Deploy staging' (#327) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #327
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
2026-03-01 20:48:31 +00:00
a8f44179a3 Don't version maps
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-03-01 15:21:34 -05:00
e1862d3917 Handle archived assets
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 15:05:10 -05:00
da96f1a090 I love gzip
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 14:49:16 -05:00
05c1107e91 Validator fixes
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 13:55:33 -05:00
83e257a4d5 Change rettention policy
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 13:39:02 -05:00
b197791509 Debian fix
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 13:24:10 -05:00
91c2d87d2f ssl fix
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 13:18:50 -05:00
225e095c92 Why do I do this to myself
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 01:41:44 -05:00
2a6099480e Drop alpine
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 01:37:48 -05:00
d6074c4b78 Compile fixes
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 01:33:41 -05:00
f3a677dc20 Attempt openssl fixes
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-01 01:29:26 -05:00
de864ac8ef Merge pull request 'Deploy Upload Escape Hatch' (#320) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #320
2026-01-06 19:20:38 +00:00
9d9ab20952 Merge pull request 'Deploy nudges and action confirmation' (#311) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #311
2025-12-28 02:06:18 +00:00
67ece176c6 Merge pull request 'Deploy script review update' (#306) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #306
2025-12-27 21:49:28 +00:00
b31b3bed5f Merge pull request 'Deploy workflow timeline' (#302) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #302
2025-12-27 08:28:42 +00:00
34cd1c7c26 Merge pull request 'Deploy dashboard update' (#299) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #299
Reviewed-by: itzaname <itzaname@noreply@itzana.me>
2025-12-27 05:41:53 +00:00
058455efd2 Merge pull request 'Deploy updates' (#291) from staging into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #291
2025-12-26 04:46:53 +00:00
76 changed files with 4235 additions and 2299 deletions

View File

@@ -24,7 +24,7 @@ steps:
- staging
- name: build-validator
image: clux/muslrust:1.91.0-stable
image: rust:1.92
commands:
- make build-validator
when:
@@ -33,7 +33,7 @@ steps:
- staging
- name: build-combobulator
image: clux/muslrust:1.91.0-stable
image: rust:1.92
commands:
- make build-combobulator
when:
@@ -184,6 +184,6 @@ steps:
- pull_request
---
kind: signature
hmac: 2d2a3b50b5864bd79efacf31f71b5a409a1782f6dbfb4669a418f577cc5517bd
hmac: a654fea05ccf642bb3a41ce777808ff995c8bd7286f2403fae179ce0db025619
...

1634
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,3 +5,13 @@ members = [
"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,10 +7,10 @@ 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 --target x86_64-unknown-linux-musl --bin maps-validation
cargo build --release --bin maps-validation
build-combobulator:
cargo build --release --target x86_64-unknown-linux-musl --bin maps-combobulator
cargo build --release --bin maps-combobulator
build-frontend:
rm -rf web/build

View File

@@ -4,12 +4,19 @@ version = "0.1.0"
edition = "2024"
[dependencies]
async-nats = "0.45.0"
async-nats.workspace = true
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1"
map-tool = { version = "2.0.0", registry = "strafesnet" }
rbx_asset = { version = "0.5.0", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" }
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
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,3 +1,4 @@
FROM alpine:3.21 AS runtime
COPY /target/x86_64-unknown-linux-musl/release/maps-combobulator /
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"]

152
combobulator/src/loader.rs Normal file
View File

@@ -0,0 +1,152 @@
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,12 +1,13 @@
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_SUBMISSION_RELEASE:&str="maptest.combobulator.submissions.release";
const SUBJECT_SEED:&str="maptest.combobulator.seed";
#[derive(Debug)]
pub enum StartupError{
@@ -55,22 +56,22 @@ async fn handle_message(
message.ack().await.map_err(HandleMessageError::Ack)?;
},
SUBJECT_SUBMISSION_BATCHRELEASE=>{
// split batch into individual messages and republish
// 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 payload=serde_json::to_vec(&submission).map_err(HandleMessageError::Json)?;
jetstream.publish(SUBJECT_SUBMISSION_RELEASE,payload.into())
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] Published individual release for submission {}",submission.SubmissionID);
println!("[combobulator] Queued seed for asset {}",seed.AssetID);
}
// ack the batch now that all individual messages are queued
message.ack().await.map_err(HandleMessageError::Ack)?;
},
SUBJECT_SUBMISSION_RELEASE=>{
let request:nats_types::ReleaseSubmissionRequest=from_slice(&message.payload)?;
processor.handle_submission_release(request).await.map_err(HandleMessageError::Process)?;
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())),
@@ -78,15 +79,11 @@ async fn handle_message(
println!("[combobulator] Message processed and acked");
Ok(())
}S
}
#[tokio::main]
async fn main()->Result<(),StartupError>{
// roblox cloud api for downloading models
let api_key=std::env::var("RBX_API_KEY").expect("RBX_API_KEY env required");
let cloud_context=rbx_asset::cloud::Context::new(rbx_asset::cloud::ApiKey::new(api_key));
// roblox cookie api for downloading assets (textures, meshes, unions)
// 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));
@@ -97,7 +94,6 @@ async fn main()->Result<(),StartupError>{
let s3_cache=s3::S3Cache::new(s3_client,s3_bucket);
let processor=process::Processor{
cloud_context,
cookie_context,
s3:s3_cache,
};
@@ -111,7 +107,7 @@ async fn main()->Result<(),StartupError>{
let filter_subjects=vec![
SUBJECT_MAPFIX_RELEASE.to_owned(),
SUBJECT_SUBMISSION_BATCHRELEASE.to_owned(),
SUBJECT_SUBMISSION_RELEASE.to_owned(),
SUBJECT_SEED.to_owned(),
];
let nats_config=async_nats::jetstream::consumer::pull::Config{

View File

@@ -7,8 +7,8 @@ pub struct ReleaseMapfixRequest{
pub TargetAssetID:u64,
}
#[expect(nonstandard_style)]
#[derive(serde::Deserialize,serde::Serialize)]
#[expect(nonstandard_style,dead_code)]
#[derive(serde::Deserialize)]
pub struct ReleaseSubmissionRequest{
pub SubmissionID:u64,
pub ReleaseDate:i64,
@@ -27,3 +27,9 @@ 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,15 +1,75 @@
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{
Download(rbx_asset::cloud::GetError),
NonFreeModel,
GetAssets(map_tool::roblox::UniqueAssetError),
ArchivedModel,
LoadDom(map_tool::roblox::LoadDomError),
DownloadAsset(map_tool::roblox::DownloadAssetError),
ConvertTexture(map_tool::roblox::ConvertTextureError),
ConvertSnf(map_tool::roblox::ConvertError),
ConvertSnf(ConvertError),
S3Get(crate::s3::GetError),
S3Put(crate::s3::PutError),
}
@@ -21,108 +81,185 @@ impl std::fmt::Display for Error{
impl std::error::Error for Error{}
pub struct Processor{
pub cloud_context:rbx_asset::cloud::Context,
pub cookie_context:rbx_asset::cookie::Context,
pub s3:S3Cache,
}
impl Processor{
/// Download a model version from Roblox cloud API.
async fn download_model(&self,model_id:u64,model_version:u64)->Result<Vec<u8>,Error>{
let location=self.cloud_context.get_asset_version_location(
rbx_asset::cloud::GetAssetVersionRequest{
asset_id:model_id,
version:model_version,
/// 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))
}
}
).await.map_err(Error::Download)?;
let location=location.location.ok_or(Error::NonFreeModel)?;
let maybe_gzip=self.cloud_context.get_asset(&location).await.map_err(Error::Download)?;
Ok(maybe_gzip.into_inner().to_vec())
}
}
/// Process a single model: extract assets, cache to S3, build SNF.
async fn process_model(&self,model_id:u64,model_version:u64)->Result<(),Error>{
println!("[combobulator] Downloading model {model_id} v{model_version}");
let rbxl_bytes=self.download_model(model_id,model_version).await?;
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_from_file(&rbxl_bytes)
.map_err(Error::GetAssets)?;
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
for id in &assets.textures{
let texture_loader=stream_iter(assets.textures).map(async|id|{
let asset_id=id.0;
let dds_key=S3Cache::texture_dds_key(asset_id);
// skip if DDS already cached
if self.s3.get(&dds_key).await.map_err(Error::S3Get)?.is_some(){
println!("[combobulator] Texture {asset_id} already cached, skipping");
continue;
}
// check raw cache, download if missing
let raw_key=S3Cache::texture_raw_key(asset_id);
let raw_data=match self.s3.get(&raw_key).await.map_err(Error::S3Get)?{
Some(cached)=>cached,
None=>{
// 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 data=map_tool::roblox::download_asset(&self.cookie_context,asset_id)
.await.map_err(Error::DownloadAsset)?;
self.s3.put(&raw_key,data.clone()).await.map_err(Error::S3Put)?;
data
},
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
};
// convert to DDS and upload
let dds=map_tool::roblox::convert_texture_to_dds(&raw_data)
.map_err(Error::ConvertTexture)?;
self.s3.put(&dds_key,dds).await.map_err(Error::S3Put)?;
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
for id in &assets.meshes{
let mesh_loader=stream_iter(assets.meshes).map(async|id|{
let asset_id=id.0;
let mesh_key=S3Cache::mesh_key(asset_id);
if self.s3.get(&mesh_key).await.map_err(Error::S3Get)?.is_some(){
println!("[combobulator] Mesh {asset_id} already cached, skipping");
continue;
}
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);
};
println!("[combobulator] Downloading mesh {asset_id}");
let data=map_tool::roblox::download_asset(&self.cookie_context,asset_id)
.await.map_err(Error::DownloadAsset)?;
self.s3.put(&mesh_key,data).await.map_err(Error::S3Put)?;
// 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
for id in &assets.unions{
let mesh_loader=stream_iter(assets.unions).map(async|id|{
let asset_id=id.0;
let union_key=S3Cache::union_key(asset_id);
if self.s3.get(&union_key).await.map_err(Error::S3Get)?.is_some(){
println!("[combobulator] Union {asset_id} already cached, skipping");
continue;
}
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);
};
println!("[combobulator] Downloading union {asset_id}");
let data=map_tool::roblox::download_asset(&self.cookie_context,asset_id)
.await.map_err(Error::DownloadAsset)?;
self.s3.put(&union_key,data).await.map_err(Error::S3Put)?;
// 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 output=map_tool::roblox::convert_to_snf(&rbxl_bytes)
let snf=convert_to_snf(dom,mesh_loader,texture_loader)
.map_err(Error::ConvertSnf)?;
let snf_key=S3Cache::snf_key(model_id,model_version);
self.s3.put(&snf_key,output.snf).await.map_err(Error::S3Put)?;
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(())
@@ -130,15 +267,14 @@ impl Processor{
/// Handle a mapfix release message.
pub async fn handle_mapfix_release(&self,request:ReleaseMapfixRequest)->Result<(),Error>{
println!("[combobulator] Processing mapfix {} (model {} v{})",
request.MapfixID,request.ModelID,request.ModelVersion);
self.process_model(request.ModelID,request.ModelVersion).await
println!("[combobulator] Processing mapfix {} (asset {})",
request.MapfixID,request.TargetAssetID);
self.process_model(request.TargetAssetID).await
}
/// Handle an individual submission release message.
pub async fn handle_submission_release(&self,request:crate::nats_types::ReleaseSubmissionRequest)->Result<(),Error>{
println!("[combobulator] Processing submission {} (model {} v{})",
request.SubmissionID,request.ModelID,request.ModelVersion);
self.process_model(request.ModelID,request.ModelVersion).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

@@ -90,7 +90,7 @@ impl S3Cache{
format!("assets/unions/{asset_id}")
}
pub fn snf_key(model_id:u64,model_version:u64)->String{
format!("maps/{model_id}/v{model_version}/map.snfm")
pub fn snf_key(model_id:u64)->String{
format!("maps/{model_id}.snfm")
}
}

View File

@@ -115,6 +115,46 @@ 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,6 +108,46 @@
}
}
}
},
"/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,6 +133,31 @@ 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 swag init -g ./cmd/maps-service/service.go
//go:generate go run github.com/swaggo/swag/cmd/swag@latest 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-20251228204118-c20dbb42afec
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301211036-f2db3cb46e8c
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.39.0
go.opentelemetry.io/otel/metric v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
google.golang.org/grpc v1.48.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.25.12
@@ -32,6 +32,25 @@ 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

48
go.sum
View File

@@ -4,6 +4,10 @@ git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed h1:eGWIQ
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/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=
@@ -14,6 +18,44 @@ 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=
@@ -237,10 +279,16 @@ 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=

View File

@@ -184,6 +184,29 @@ 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
@@ -212,6 +235,21 @@ 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

View File

@@ -4,6 +4,7 @@ package api
import (
"net/http"
"strings"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware"
@@ -82,18 +83,8 @@ func (o otelOptionFunc) applyServer(c *serverConfig) {
func newServerConfig(opts ...ServerOption) serverConfig {
cfg := serverConfig{
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)
},
NotFound: http.NotFound,
MethodNotAllowed: nil,
ErrorHandler: ogenerrors.DefaultErrorHandler,
Middleware: nil,
MaxMultipartMemory: 32 << 20, // 32 MB
@@ -116,8 +107,44 @@ func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) {
s.cfg.NotFound(w, r)
}
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, allowed string) {
s.cfg.MethodNotAllowed(w, r, allowed)
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 (cfg serverConfig) baseServer() (s baseServer, err error) {

View File

@@ -17,7 +17,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.opentelemetry.io/otel/trace"
)
@@ -193,6 +193,12 @@ type Invoker interface {
//
// POST /usernames
BatchUsernames(ctx context.Context, request *BatchUsernamesReq) (*BatchUsernamesOK, error)
// CombobulateMap invokes combobulateMap operation.
//
// Queue a map for combobulator processing.
//
// POST /maps/{MapID}/combobulate
CombobulateMap(ctx context.Context, params CombobulateMapParams) error
// CreateMapfix invokes createMapfix operation.
//
// Trigger the validator to create a mapfix.
@@ -355,6 +361,12 @@ type Invoker interface {
//
// POST /release-submissions
ReleaseSubmissions(ctx context.Context, request []ReleaseInfo) (*OperationID, error)
// SeedCombobulator invokes seedCombobulator operation.
//
// Queue all maps for combobulator processing.
//
// POST /maps-admin/seed-combobulator
SeedCombobulator(ctx context.Context) error
// SessionRoles invokes sessionRoles operation.
//
// Get list of roles for the current session.
@@ -423,14 +435,6 @@ type Client struct {
sec SecuritySource
baseClient
}
type errorHandler interface {
NewError(ctx context.Context, err error) *ErrorStatusCode
}
var _ Handler = struct {
errorHandler
*Client
}{}
// NewClient initializes new Client defined by OAS.
func NewClient(serverURL string, sec SecuritySource, opts ...ClientOption) (*Client, error) {
@@ -580,7 +584,8 @@ func (c *Client) sendActionMapfixAccepted(ctx context.Context, params ActionMapf
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixAcceptedResponse(resp)
@@ -705,7 +710,8 @@ func (c *Client) sendActionMapfixReject(ctx context.Context, params ActionMapfix
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixRejectResponse(resp)
@@ -830,7 +836,8 @@ func (c *Client) sendActionMapfixRequestChanges(ctx context.Context, params Acti
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixRequestChangesResponse(resp)
@@ -956,7 +963,8 @@ func (c *Client) sendActionMapfixResetSubmitting(ctx context.Context, params Act
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixResetSubmittingResponse(resp)
@@ -1081,7 +1089,8 @@ func (c *Client) sendActionMapfixRetryValidate(ctx context.Context, params Actio
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixRetryValidateResponse(resp)
@@ -1206,7 +1215,8 @@ func (c *Client) sendActionMapfixRevoke(ctx context.Context, params ActionMapfix
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixRevokeResponse(resp)
@@ -1331,7 +1341,8 @@ func (c *Client) sendActionMapfixTriggerRelease(ctx context.Context, params Acti
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixTriggerReleaseResponse(resp)
@@ -1456,7 +1467,8 @@ func (c *Client) sendActionMapfixTriggerSubmit(ctx context.Context, params Actio
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixTriggerSubmitResponse(resp)
@@ -1581,7 +1593,8 @@ func (c *Client) sendActionMapfixTriggerSubmitUnchecked(ctx context.Context, par
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixTriggerSubmitUncheckedResponse(resp)
@@ -1706,7 +1719,8 @@ func (c *Client) sendActionMapfixTriggerUpload(ctx context.Context, params Actio
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixTriggerUploadResponse(resp)
@@ -1831,7 +1845,8 @@ func (c *Client) sendActionMapfixTriggerValidate(ctx context.Context, params Act
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixTriggerValidateResponse(resp)
@@ -1956,7 +1971,8 @@ func (c *Client) sendActionMapfixUploaded(ctx context.Context, params ActionMapf
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixUploadedResponse(resp)
@@ -2081,7 +2097,8 @@ func (c *Client) sendActionMapfixValidated(ctx context.Context, params ActionMap
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionMapfixValidatedResponse(resp)
@@ -2206,7 +2223,8 @@ func (c *Client) sendActionSubmissionAccepted(ctx context.Context, params Action
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionAcceptedResponse(resp)
@@ -2331,7 +2349,8 @@ func (c *Client) sendActionSubmissionReject(ctx context.Context, params ActionSu
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionRejectResponse(resp)
@@ -2456,7 +2475,8 @@ func (c *Client) sendActionSubmissionRequestChanges(ctx context.Context, params
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionRequestChangesResponse(resp)
@@ -2582,7 +2602,8 @@ func (c *Client) sendActionSubmissionResetSubmitting(ctx context.Context, params
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionResetSubmittingResponse(resp)
@@ -2707,7 +2728,8 @@ func (c *Client) sendActionSubmissionRetryValidate(ctx context.Context, params A
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionRetryValidateResponse(resp)
@@ -2832,7 +2854,8 @@ func (c *Client) sendActionSubmissionRevoke(ctx context.Context, params ActionSu
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionRevokeResponse(resp)
@@ -2957,7 +2980,8 @@ func (c *Client) sendActionSubmissionTriggerSubmit(ctx context.Context, params A
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionTriggerSubmitResponse(resp)
@@ -3082,7 +3106,8 @@ func (c *Client) sendActionSubmissionTriggerSubmitUnchecked(ctx context.Context,
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionTriggerSubmitUncheckedResponse(resp)
@@ -3207,7 +3232,8 @@ func (c *Client) sendActionSubmissionTriggerUpload(ctx context.Context, params A
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionTriggerUploadResponse(resp)
@@ -3332,7 +3358,8 @@ func (c *Client) sendActionSubmissionTriggerValidate(ctx context.Context, params
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionTriggerValidateResponse(resp)
@@ -3458,7 +3485,8 @@ func (c *Client) sendActionSubmissionValidated(ctx context.Context, params Actio
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeActionSubmissionValidatedResponse(resp)
@@ -3534,7 +3562,8 @@ func (c *Client) sendBatchAssetThumbnails(ctx context.Context, request *BatchAss
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeBatchAssetThumbnailsResponse(resp)
@@ -3610,7 +3639,8 @@ func (c *Client) sendBatchUserThumbnails(ctx context.Context, request *BatchUser
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeBatchUserThumbnailsResponse(resp)
@@ -3686,7 +3716,8 @@ func (c *Client) sendBatchUsernames(ctx context.Context, request *BatchUsernames
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeBatchUsernamesResponse(resp)
@@ -3697,6 +3728,132 @@ func (c *Client) sendBatchUsernames(ctx context.Context, request *BatchUsernames
return result, nil
}
// CombobulateMap invokes combobulateMap operation.
//
// Queue a map for combobulator processing.
//
// POST /maps/{MapID}/combobulate
func (c *Client) CombobulateMap(ctx context.Context, params CombobulateMapParams) error {
_, err := c.sendCombobulateMap(ctx, params)
return err
}
func (c *Client) sendCombobulateMap(ctx context.Context, params CombobulateMapParams) (res *CombobulateMapNoContent, err error) {
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("combobulateMap"),
semconv.HTTPRequestMethodKey.String("POST"),
semconv.URLTemplateKey.String("/maps/{MapID}/combobulate"),
}
otelAttrs = append(otelAttrs, c.cfg.Attributes...)
// Run stopwatch.
startTime := time.Now()
defer func() {
// Use floating point division here for higher precision (instead of Millisecond method).
elapsedDuration := time.Since(startTime)
c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
// Start a span for this request.
ctx, span := c.cfg.Tracer.Start(ctx, CombobulateMapOperation,
trace.WithAttributes(otelAttrs...),
clientSpanKind,
)
// Track stage for error reporting.
var stage string
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
span.End()
}()
stage = "BuildURL"
u := uri.Clone(c.requestURL(ctx))
var pathParts [3]string
pathParts[0] = "/maps/"
{
// Encode "MapID" parameter.
e := uri.NewPathEncoder(uri.PathEncoderConfig{
Param: "MapID",
Style: uri.PathStyleSimple,
Explode: false,
})
if err := func() error {
return e.EncodeValue(conv.Int64ToString(params.MapID))
}(); err != nil {
return res, errors.Wrap(err, "encode path")
}
encoded, err := e.Result()
if err != nil {
return res, errors.Wrap(err, "encode path")
}
pathParts[1] = encoded
}
pathParts[2] = "/combobulate"
uri.AddPathParts(u, pathParts[:]...)
stage = "EncodeRequest"
r, err := ht.NewRequest(ctx, "POST", u)
if err != nil {
return res, errors.Wrap(err, "create request")
}
{
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CombobulateMapOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
}
}
if ok := func() bool {
nextRequirement:
for _, requirement := range []bitset{
{0b00000001},
} {
for i, mask := range requirement {
if satisfied[i]&mask != mask {
continue nextRequirement
}
}
return true
}
return false
}(); !ok {
return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied
}
}
stage = "SendRequest"
resp, err := c.cfg.Client.Do(r)
if err != nil {
return res, errors.Wrap(err, "do request")
}
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCombobulateMapResponse(resp)
if err != nil {
return res, errors.Wrap(err, "decode response")
}
return result, nil
}
// CreateMapfix invokes createMapfix operation.
//
// Trigger the validator to create a mapfix.
@@ -3795,7 +3952,8 @@ func (c *Client) sendCreateMapfix(ctx context.Context, request *MapfixTriggerCre
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCreateMapfixResponse(resp)
@@ -3923,7 +4081,8 @@ func (c *Client) sendCreateMapfixAuditComment(ctx context.Context, request Creat
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCreateMapfixAuditCommentResponse(resp)
@@ -4032,7 +4191,8 @@ func (c *Client) sendCreateScript(ctx context.Context, request *ScriptCreate) (r
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCreateScriptResponse(resp)
@@ -4141,7 +4301,8 @@ func (c *Client) sendCreateScriptPolicy(ctx context.Context, request *ScriptPoli
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCreateScriptPolicyResponse(resp)
@@ -4250,7 +4411,8 @@ func (c *Client) sendCreateSubmission(ctx context.Context, request *SubmissionTr
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCreateSubmissionResponse(resp)
@@ -4359,7 +4521,8 @@ func (c *Client) sendCreateSubmissionAdmin(ctx context.Context, request *Submiss
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCreateSubmissionAdminResponse(resp)
@@ -4487,7 +4650,8 @@ func (c *Client) sendCreateSubmissionAuditComment(ctx context.Context, request C
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeCreateSubmissionAuditCommentResponse(resp)
@@ -4611,7 +4775,8 @@ func (c *Client) sendDeleteScript(ctx context.Context, params DeleteScriptParams
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeDeleteScriptResponse(resp)
@@ -4735,7 +4900,8 @@ func (c *Client) sendDeleteScriptPolicy(ctx context.Context, params DeleteScript
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeDeleteScriptPolicyResponse(resp)
@@ -4860,7 +5026,8 @@ func (c *Client) sendDownloadMapAsset(ctx context.Context, params DownloadMapAss
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeDownloadMapAssetResponse(resp)
@@ -4972,7 +5139,8 @@ func (c *Client) sendGetAssetThumbnail(ctx context.Context, params GetAssetThumb
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetAssetThumbnailResponse(resp)
@@ -5063,7 +5231,8 @@ func (c *Client) sendGetMap(ctx context.Context, params GetMapParams) (res *Map,
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetMapResponse(resp)
@@ -5154,7 +5323,8 @@ func (c *Client) sendGetMapfix(ctx context.Context, params GetMapfixParams) (res
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetMapfixResponse(resp)
@@ -5278,7 +5448,8 @@ func (c *Client) sendGetOperation(ctx context.Context, params GetOperationParams
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetOperationResponse(resp)
@@ -5369,7 +5540,8 @@ func (c *Client) sendGetScript(ctx context.Context, params GetScriptParams) (res
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetScriptResponse(resp)
@@ -5460,7 +5632,8 @@ func (c *Client) sendGetScriptPolicy(ctx context.Context, params GetScriptPolicy
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetScriptPolicyResponse(resp)
@@ -5533,7 +5706,8 @@ func (c *Client) sendGetStats(ctx context.Context) (res *Stats, err error) {
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetStatsResponse(resp)
@@ -5624,7 +5798,8 @@ func (c *Client) sendGetSubmission(ctx context.Context, params GetSubmissionPara
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetSubmissionResponse(resp)
@@ -5736,7 +5911,8 @@ func (c *Client) sendGetUserThumbnail(ctx context.Context, params GetUserThumbna
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeGetUserThumbnailResponse(resp)
@@ -5860,7 +6036,8 @@ func (c *Client) sendListMapfixAuditEvents(ctx context.Context, params ListMapfi
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeListMapfixAuditEventsResponse(resp)
@@ -6118,7 +6295,8 @@ func (c *Client) sendListMapfixes(ctx context.Context, params ListMapfixesParams
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeListMapfixesResponse(resp)
@@ -6291,7 +6469,8 @@ func (c *Client) sendListMaps(ctx context.Context, params ListMapsParams) (res [
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeListMapsResponse(resp)
@@ -6447,7 +6626,8 @@ func (c *Client) sendListScriptPolicy(ctx context.Context, params ListScriptPoli
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeListScriptPolicyResponse(resp)
@@ -6637,7 +6817,8 @@ func (c *Client) sendListScripts(ctx context.Context, params ListScriptsParams)
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeListScriptsResponse(resp)
@@ -6761,7 +6942,8 @@ func (c *Client) sendListSubmissionAuditEvents(ctx context.Context, params ListS
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeListSubmissionAuditEventsResponse(resp)
@@ -7019,7 +7201,8 @@ func (c *Client) sendListSubmissions(ctx context.Context, params ListSubmissions
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeListSubmissionsResponse(resp)
@@ -7128,7 +7311,8 @@ func (c *Client) sendReleaseSubmissions(ctx context.Context, request []ReleaseIn
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeReleaseSubmissionsResponse(resp)
@@ -7139,6 +7323,113 @@ func (c *Client) sendReleaseSubmissions(ctx context.Context, request []ReleaseIn
return result, nil
}
// SeedCombobulator invokes seedCombobulator operation.
//
// Queue all maps for combobulator processing.
//
// POST /maps-admin/seed-combobulator
func (c *Client) SeedCombobulator(ctx context.Context) error {
_, err := c.sendSeedCombobulator(ctx)
return err
}
func (c *Client) sendSeedCombobulator(ctx context.Context) (res *SeedCombobulatorNoContent, err error) {
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("seedCombobulator"),
semconv.HTTPRequestMethodKey.String("POST"),
semconv.URLTemplateKey.String("/maps-admin/seed-combobulator"),
}
otelAttrs = append(otelAttrs, c.cfg.Attributes...)
// Run stopwatch.
startTime := time.Now()
defer func() {
// Use floating point division here for higher precision (instead of Millisecond method).
elapsedDuration := time.Since(startTime)
c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
// Start a span for this request.
ctx, span := c.cfg.Tracer.Start(ctx, SeedCombobulatorOperation,
trace.WithAttributes(otelAttrs...),
clientSpanKind,
)
// Track stage for error reporting.
var stage string
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
span.End()
}()
stage = "BuildURL"
u := uri.Clone(c.requestURL(ctx))
var pathParts [1]string
pathParts[0] = "/maps-admin/seed-combobulator"
uri.AddPathParts(u, pathParts[:]...)
stage = "EncodeRequest"
r, err := ht.NewRequest(ctx, "POST", u)
if err != nil {
return res, errors.Wrap(err, "create request")
}
{
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, SeedCombobulatorOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
}
}
if ok := func() bool {
nextRequirement:
for _, requirement := range []bitset{
{0b00000001},
} {
for i, mask := range requirement {
if satisfied[i]&mask != mask {
continue nextRequirement
}
}
return true
}
return false
}(); !ok {
return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied
}
}
stage = "SendRequest"
resp, err := c.cfg.Client.Do(r)
if err != nil {
return res, errors.Wrap(err, "do request")
}
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeSeedCombobulatorResponse(resp)
if err != nil {
return res, errors.Wrap(err, "decode response")
}
return result, nil
}
// SessionRoles invokes sessionRoles operation.
//
// Get list of roles for the current session.
@@ -7234,7 +7525,8 @@ func (c *Client) sendSessionRoles(ctx context.Context) (res *Roles, err error) {
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeSessionRolesResponse(resp)
@@ -7340,7 +7632,8 @@ func (c *Client) sendSessionUser(ctx context.Context) (res *User, err error) {
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeSessionUserResponse(resp)
@@ -7446,7 +7739,8 @@ func (c *Client) sendSessionValidate(ctx context.Context) (res bool, err error)
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeSessionValidateResponse(resp)
@@ -7571,7 +7865,8 @@ func (c *Client) sendSetMapfixCompleted(ctx context.Context, params SetMapfixCom
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeSetMapfixCompletedResponse(resp)
@@ -7696,7 +7991,8 @@ func (c *Client) sendSetSubmissionCompleted(ctx context.Context, params SetSubmi
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeSetSubmissionCompletedResponse(resp)
@@ -7824,7 +8120,8 @@ func (c *Client) sendUpdateMapfixDescription(ctx context.Context, request Update
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeUpdateMapfixDescriptionResponse(resp)
@@ -7981,7 +8278,8 @@ func (c *Client) sendUpdateMapfixModel(ctx context.Context, params UpdateMapfixM
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeUpdateMapfixModelResponse(resp)
@@ -8108,7 +8406,8 @@ func (c *Client) sendUpdateScript(ctx context.Context, request *ScriptUpdate, pa
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeUpdateScriptResponse(resp)
@@ -8235,7 +8534,8 @@ func (c *Client) sendUpdateScriptPolicy(ctx context.Context, request *ScriptPoli
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeUpdateScriptPolicyResponse(resp)
@@ -8392,7 +8692,8 @@ func (c *Client) sendUpdateSubmissionModel(ctx context.Context, params UpdateSub
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
body := resp.Body
defer body.Close()
stage = "DecodeResponse"
result, err := decodeUpdateSubmissionModelResponse(resp)

View File

@@ -15,7 +15,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.opentelemetry.io/otel/trace"
)
@@ -46,6 +46,8 @@ func (s *Server) handleActionMapfixAcceptedRequest(args [1]string, argsEscaped b
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/reset-validating"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixAcceptedOperation,
@@ -244,6 +246,8 @@ func (s *Server) handleActionMapfixRejectRequest(args [1]string, argsEscaped boo
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/reject"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixRejectOperation,
@@ -442,6 +446,8 @@ func (s *Server) handleActionMapfixRequestChangesRequest(args [1]string, argsEsc
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/request-changes"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixRequestChangesOperation,
@@ -641,6 +647,8 @@ func (s *Server) handleActionMapfixResetSubmittingRequest(args [1]string, argsEs
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/reset-submitting"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixResetSubmittingOperation,
@@ -839,6 +847,8 @@ func (s *Server) handleActionMapfixRetryValidateRequest(args [1]string, argsEsca
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/retry-validate"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixRetryValidateOperation,
@@ -1037,6 +1047,8 @@ func (s *Server) handleActionMapfixRevokeRequest(args [1]string, argsEscaped boo
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/revoke"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixRevokeOperation,
@@ -1235,6 +1247,8 @@ func (s *Server) handleActionMapfixTriggerReleaseRequest(args [1]string, argsEsc
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/trigger-release"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixTriggerReleaseOperation,
@@ -1433,6 +1447,8 @@ func (s *Server) handleActionMapfixTriggerSubmitRequest(args [1]string, argsEsca
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/trigger-submit"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixTriggerSubmitOperation,
@@ -1631,6 +1647,8 @@ func (s *Server) handleActionMapfixTriggerSubmitUncheckedRequest(args [1]string,
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/trigger-submit-unchecked"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixTriggerSubmitUncheckedOperation,
@@ -1829,6 +1847,8 @@ func (s *Server) handleActionMapfixTriggerUploadRequest(args [1]string, argsEsca
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/trigger-upload"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixTriggerUploadOperation,
@@ -2027,6 +2047,8 @@ func (s *Server) handleActionMapfixTriggerValidateRequest(args [1]string, argsEs
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/trigger-validate"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixTriggerValidateOperation,
@@ -2225,6 +2247,8 @@ func (s *Server) handleActionMapfixUploadedRequest(args [1]string, argsEscaped b
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/reset-releasing"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixUploadedOperation,
@@ -2423,6 +2447,8 @@ func (s *Server) handleActionMapfixValidatedRequest(args [1]string, argsEscaped
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/status/reset-uploading"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionMapfixValidatedOperation,
@@ -2621,6 +2647,8 @@ func (s *Server) handleActionSubmissionAcceptedRequest(args [1]string, argsEscap
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reset-validating"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionAcceptedOperation,
@@ -2819,6 +2847,8 @@ func (s *Server) handleActionSubmissionRejectRequest(args [1]string, argsEscaped
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reject"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionRejectOperation,
@@ -3017,6 +3047,8 @@ func (s *Server) handleActionSubmissionRequestChangesRequest(args [1]string, arg
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/request-changes"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionRequestChangesOperation,
@@ -3216,6 +3248,8 @@ func (s *Server) handleActionSubmissionResetSubmittingRequest(args [1]string, ar
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reset-submitting"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionResetSubmittingOperation,
@@ -3414,6 +3448,8 @@ func (s *Server) handleActionSubmissionRetryValidateRequest(args [1]string, args
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/retry-validate"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionRetryValidateOperation,
@@ -3612,6 +3648,8 @@ func (s *Server) handleActionSubmissionRevokeRequest(args [1]string, argsEscaped
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/revoke"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionRevokeOperation,
@@ -3810,6 +3848,8 @@ func (s *Server) handleActionSubmissionTriggerSubmitRequest(args [1]string, args
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/trigger-submit"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionTriggerSubmitOperation,
@@ -4008,6 +4048,8 @@ func (s *Server) handleActionSubmissionTriggerSubmitUncheckedRequest(args [1]str
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/trigger-submit-unchecked"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionTriggerSubmitUncheckedOperation,
@@ -4206,6 +4248,8 @@ func (s *Server) handleActionSubmissionTriggerUploadRequest(args [1]string, args
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/trigger-upload"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionTriggerUploadOperation,
@@ -4404,6 +4448,8 @@ func (s *Server) handleActionSubmissionTriggerValidateRequest(args [1]string, ar
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/trigger-validate"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionTriggerValidateOperation,
@@ -4603,6 +4649,8 @@ func (s *Server) handleActionSubmissionValidatedRequest(args [1]string, argsEsca
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/status/reset-uploading"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ActionSubmissionValidatedOperation,
@@ -4801,6 +4849,8 @@ func (s *Server) handleBatchAssetThumbnailsRequest(args [0]string, argsEscaped b
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/thumbnails/assets"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), BatchAssetThumbnailsOperation,
@@ -4953,6 +5003,8 @@ func (s *Server) handleBatchUserThumbnailsRequest(args [0]string, argsEscaped bo
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/thumbnails/users"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), BatchUserThumbnailsOperation,
@@ -5105,6 +5157,8 @@ func (s *Server) handleBatchUsernamesRequest(args [0]string, argsEscaped bool, w
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/usernames"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), BatchUsernamesOperation,
@@ -5244,6 +5298,206 @@ func (s *Server) handleBatchUsernamesRequest(args [0]string, argsEscaped bool, w
}
}
// handleCombobulateMapRequest handles combobulateMap operation.
//
// Queue a map for combobulator processing.
//
// POST /maps/{MapID}/combobulate
func (s *Server) handleCombobulateMapRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
statusWriter := &codeRecorder{ResponseWriter: w}
w = statusWriter
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("combobulateMap"),
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/maps/{MapID}/combobulate"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CombobulateMapOperation,
trace.WithAttributes(otelAttrs...),
serverSpanKind,
)
defer span.End()
// Add Labeler to context.
labeler := &Labeler{attrs: otelAttrs}
ctx = contextWithLabeler(ctx, labeler)
// Run stopwatch.
startTime := time.Now()
defer func() {
elapsedDuration := time.Since(startTime)
attrSet := labeler.AttributeSet()
attrs := attrSet.ToSlice()
code := statusWriter.status
if code != 0 {
codeAttr := semconv.HTTPResponseStatusCode(code)
attrs = append(attrs, codeAttr)
span.SetAttributes(codeAttr)
}
attrOpt := metric.WithAttributes(attrs...)
// Increment request counter.
s.requests.Add(ctx, 1, attrOpt)
// Use floating point division here for higher precision (instead of Millisecond method).
s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt)
}()
var (
recordError = func(stage string, err error) {
span.RecordError(err)
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
// Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges,
// unless there was another error (e.g., network error receiving the response body; or 3xx codes with
// max redirects exceeded), in which case status MUST be set to Error.
code := statusWriter.status
if code < 100 || code >= 500 {
span.SetStatus(codes.Error, stage)
}
attrSet := labeler.AttributeSet()
attrs := attrSet.ToSlice()
if code != 0 {
attrs = append(attrs, semconv.HTTPResponseStatusCode(code))
}
s.errors.Add(ctx, 1, metric.WithAttributes(attrs...))
}
err error
opErrContext = ogenerrors.OperationContext{
Name: CombobulateMapOperation,
ID: "combobulateMap",
}
)
{
type bitset = [1]uint8
var satisfied bitset
{
sctx, ok, err := s.securityCookieAuth(ctx, CombobulateMapOperation, r)
if err != nil {
err = &ogenerrors.SecurityError{
OperationContext: opErrContext,
Security: "CookieAuth",
Err: err,
}
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
defer recordError("Security:CookieAuth", err)
}
return
}
if ok {
satisfied[0] |= 1 << 0
ctx = sctx
}
}
if ok := func() bool {
nextRequirement:
for _, requirement := range []bitset{
{0b00000001},
} {
for i, mask := range requirement {
if satisfied[i]&mask != mask {
continue nextRequirement
}
}
return true
}
return false
}(); !ok {
err = &ogenerrors.SecurityError{
OperationContext: opErrContext,
Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied,
}
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
defer recordError("Security", err)
}
return
}
}
params, err := decodeCombobulateMapParams(args, argsEscaped, r)
if err != nil {
err = &ogenerrors.DecodeParamsError{
OperationContext: opErrContext,
Err: err,
}
defer recordError("DecodeParams", err)
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
var rawBody []byte
var response *CombobulateMapNoContent
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
Context: ctx,
OperationName: CombobulateMapOperation,
OperationSummary: "Queue a map for combobulator processing",
OperationID: "combobulateMap",
Body: nil,
RawBody: rawBody,
Params: middleware.Parameters{
{
Name: "MapID",
In: "path",
}: params.MapID,
},
Raw: r,
}
type (
Request = struct{}
Params = CombobulateMapParams
Response = *CombobulateMapNoContent
)
response, err = middleware.HookMiddleware[
Request,
Params,
Response,
](
m,
mreq,
unpackCombobulateMapParams,
func(ctx context.Context, request Request, params Params) (response Response, err error) {
err = s.h.CombobulateMap(ctx, params)
return response, err
},
)
} else {
err = s.h.CombobulateMap(ctx, params)
}
if err != nil {
if errRes, ok := errors.Into[*ErrorStatusCode](err); ok {
if err := encodeErrorResponse(errRes, w, span); err != nil {
defer recordError("Internal", err)
}
return
}
if errors.Is(err, ht.ErrNotImplemented) {
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil {
defer recordError("Internal", err)
}
return
}
if err := encodeCombobulateMapResponse(response, w, span); err != nil {
defer recordError("EncodeResponse", err)
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
s.cfg.ErrorHandler(ctx, w, r, err)
}
return
}
}
// handleCreateMapfixRequest handles createMapfix operation.
//
// Trigger the validator to create a mapfix.
@@ -5257,6 +5511,8 @@ func (s *Server) handleCreateMapfixRequest(args [0]string, argsEscaped bool, w h
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CreateMapfixOperation,
@@ -5455,6 +5711,8 @@ func (s *Server) handleCreateMapfixAuditCommentRequest(args [1]string, argsEscap
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/comment"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CreateMapfixAuditCommentOperation,
@@ -5668,6 +5926,8 @@ func (s *Server) handleCreateScriptRequest(args [0]string, argsEscaped bool, w h
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/scripts"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CreateScriptOperation,
@@ -5866,6 +6126,8 @@ func (s *Server) handleCreateScriptPolicyRequest(args [0]string, argsEscaped boo
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/script-policy"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CreateScriptPolicyOperation,
@@ -6064,6 +6326,8 @@ func (s *Server) handleCreateSubmissionRequest(args [0]string, argsEscaped bool,
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CreateSubmissionOperation,
@@ -6262,6 +6526,8 @@ func (s *Server) handleCreateSubmissionAdminRequest(args [0]string, argsEscaped
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions-admin"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CreateSubmissionAdminOperation,
@@ -6460,6 +6726,8 @@ func (s *Server) handleCreateSubmissionAuditCommentRequest(args [1]string, argsE
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/comment"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), CreateSubmissionAuditCommentOperation,
@@ -6673,6 +6941,8 @@ func (s *Server) handleDeleteScriptRequest(args [1]string, argsEscaped bool, w h
semconv.HTTPRequestMethodKey.String("DELETE"),
semconv.HTTPRouteKey.String("/scripts/{ScriptID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), DeleteScriptOperation,
@@ -6871,6 +7141,8 @@ func (s *Server) handleDeleteScriptPolicyRequest(args [1]string, argsEscaped boo
semconv.HTTPRequestMethodKey.String("DELETE"),
semconv.HTTPRouteKey.String("/script-policy/{ScriptPolicyID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), DeleteScriptPolicyOperation,
@@ -7069,6 +7341,8 @@ func (s *Server) handleDownloadMapAssetRequest(args [1]string, argsEscaped bool,
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/maps/{MapID}/download"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), DownloadMapAssetOperation,
@@ -7267,6 +7541,8 @@ func (s *Server) handleGetAssetThumbnailRequest(args [1]string, argsEscaped bool
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/thumbnails/asset/{AssetID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetAssetThumbnailOperation,
@@ -7423,6 +7699,8 @@ func (s *Server) handleGetMapRequest(args [1]string, argsEscaped bool, w http.Re
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/maps/{MapID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetMapOperation,
@@ -7575,6 +7853,8 @@ func (s *Server) handleGetMapfixRequest(args [1]string, argsEscaped bool, w http
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetMapfixOperation,
@@ -7727,6 +8007,8 @@ func (s *Server) handleGetOperationRequest(args [1]string, argsEscaped bool, w h
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/operations/{OperationID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetOperationOperation,
@@ -7925,6 +8207,8 @@ func (s *Server) handleGetScriptRequest(args [1]string, argsEscaped bool, w http
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/scripts/{ScriptID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetScriptOperation,
@@ -8077,6 +8361,8 @@ func (s *Server) handleGetScriptPolicyRequest(args [1]string, argsEscaped bool,
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/script-policy/{ScriptPolicyID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetScriptPolicyOperation,
@@ -8229,6 +8515,8 @@ func (s *Server) handleGetStatsRequest(args [0]string, argsEscaped bool, w http.
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/stats"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetStatsOperation,
@@ -8362,6 +8650,8 @@ func (s *Server) handleGetSubmissionRequest(args [1]string, argsEscaped bool, w
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetSubmissionOperation,
@@ -8514,6 +8804,8 @@ func (s *Server) handleGetUserThumbnailRequest(args [1]string, argsEscaped bool,
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/thumbnails/user/{UserID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), GetUserThumbnailOperation,
@@ -8670,6 +8962,8 @@ func (s *Server) handleListMapfixAuditEventsRequest(args [1]string, argsEscaped
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/audit-events"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ListMapfixAuditEventsOperation,
@@ -8830,6 +9124,8 @@ func (s *Server) handleListMapfixesRequest(args [0]string, argsEscaped bool, w h
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/mapfixes"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ListMapfixesOperation,
@@ -9022,6 +9318,8 @@ func (s *Server) handleListMapsRequest(args [0]string, argsEscaped bool, w http.
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/maps"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ListMapsOperation,
@@ -9194,6 +9492,8 @@ func (s *Server) handleListScriptPolicyRequest(args [0]string, argsEscaped bool,
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/script-policy"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ListScriptPolicyOperation,
@@ -9362,6 +9662,8 @@ func (s *Server) handleListScriptsRequest(args [0]string, argsEscaped bool, w ht
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/scripts"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ListScriptsOperation,
@@ -9538,6 +9840,8 @@ func (s *Server) handleListSubmissionAuditEventsRequest(args [1]string, argsEsca
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/audit-events"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ListSubmissionAuditEventsOperation,
@@ -9698,6 +10002,8 @@ func (s *Server) handleListSubmissionsRequest(args [0]string, argsEscaped bool,
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/submissions"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ListSubmissionsOperation,
@@ -9890,6 +10196,8 @@ func (s *Server) handleReleaseSubmissionsRequest(args [0]string, argsEscaped boo
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/release-submissions"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), ReleaseSubmissionsOperation,
@@ -10075,6 +10383,191 @@ func (s *Server) handleReleaseSubmissionsRequest(args [0]string, argsEscaped boo
}
}
// handleSeedCombobulatorRequest handles seedCombobulator operation.
//
// Queue all maps for combobulator processing.
//
// POST /maps-admin/seed-combobulator
func (s *Server) handleSeedCombobulatorRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
statusWriter := &codeRecorder{ResponseWriter: w}
w = statusWriter
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("seedCombobulator"),
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/maps-admin/seed-combobulator"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), SeedCombobulatorOperation,
trace.WithAttributes(otelAttrs...),
serverSpanKind,
)
defer span.End()
// Add Labeler to context.
labeler := &Labeler{attrs: otelAttrs}
ctx = contextWithLabeler(ctx, labeler)
// Run stopwatch.
startTime := time.Now()
defer func() {
elapsedDuration := time.Since(startTime)
attrSet := labeler.AttributeSet()
attrs := attrSet.ToSlice()
code := statusWriter.status
if code != 0 {
codeAttr := semconv.HTTPResponseStatusCode(code)
attrs = append(attrs, codeAttr)
span.SetAttributes(codeAttr)
}
attrOpt := metric.WithAttributes(attrs...)
// Increment request counter.
s.requests.Add(ctx, 1, attrOpt)
// Use floating point division here for higher precision (instead of Millisecond method).
s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt)
}()
var (
recordError = func(stage string, err error) {
span.RecordError(err)
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status
// Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges,
// unless there was another error (e.g., network error receiving the response body; or 3xx codes with
// max redirects exceeded), in which case status MUST be set to Error.
code := statusWriter.status
if code < 100 || code >= 500 {
span.SetStatus(codes.Error, stage)
}
attrSet := labeler.AttributeSet()
attrs := attrSet.ToSlice()
if code != 0 {
attrs = append(attrs, semconv.HTTPResponseStatusCode(code))
}
s.errors.Add(ctx, 1, metric.WithAttributes(attrs...))
}
err error
opErrContext = ogenerrors.OperationContext{
Name: SeedCombobulatorOperation,
ID: "seedCombobulator",
}
)
{
type bitset = [1]uint8
var satisfied bitset
{
sctx, ok, err := s.securityCookieAuth(ctx, SeedCombobulatorOperation, r)
if err != nil {
err = &ogenerrors.SecurityError{
OperationContext: opErrContext,
Security: "CookieAuth",
Err: err,
}
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
defer recordError("Security:CookieAuth", err)
}
return
}
if ok {
satisfied[0] |= 1 << 0
ctx = sctx
}
}
if ok := func() bool {
nextRequirement:
for _, requirement := range []bitset{
{0b00000001},
} {
for i, mask := range requirement {
if satisfied[i]&mask != mask {
continue nextRequirement
}
}
return true
}
return false
}(); !ok {
err = &ogenerrors.SecurityError{
OperationContext: opErrContext,
Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied,
}
if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil {
defer recordError("Security", err)
}
return
}
}
var rawBody []byte
var response *SeedCombobulatorNoContent
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
Context: ctx,
OperationName: SeedCombobulatorOperation,
OperationSummary: "Queue all maps for combobulator processing",
OperationID: "seedCombobulator",
Body: nil,
RawBody: rawBody,
Params: middleware.Parameters{},
Raw: r,
}
type (
Request = struct{}
Params = struct{}
Response = *SeedCombobulatorNoContent
)
response, err = middleware.HookMiddleware[
Request,
Params,
Response,
](
m,
mreq,
nil,
func(ctx context.Context, request Request, params Params) (response Response, err error) {
err = s.h.SeedCombobulator(ctx)
return response, err
},
)
} else {
err = s.h.SeedCombobulator(ctx)
}
if err != nil {
if errRes, ok := errors.Into[*ErrorStatusCode](err); ok {
if err := encodeErrorResponse(errRes, w, span); err != nil {
defer recordError("Internal", err)
}
return
}
if errors.Is(err, ht.ErrNotImplemented) {
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil {
defer recordError("Internal", err)
}
return
}
if err := encodeSeedCombobulatorResponse(response, w, span); err != nil {
defer recordError("EncodeResponse", err)
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
s.cfg.ErrorHandler(ctx, w, r, err)
}
return
}
}
// handleSessionRolesRequest handles sessionRoles operation.
//
// Get list of roles for the current session.
@@ -10088,6 +10581,8 @@ func (s *Server) handleSessionRolesRequest(args [0]string, argsEscaped bool, w h
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/session/roles"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), SessionRolesOperation,
@@ -10271,6 +10766,8 @@ func (s *Server) handleSessionUserRequest(args [0]string, argsEscaped bool, w ht
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/session/user"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), SessionUserOperation,
@@ -10454,6 +10951,8 @@ func (s *Server) handleSessionValidateRequest(args [0]string, argsEscaped bool,
semconv.HTTPRequestMethodKey.String("GET"),
semconv.HTTPRouteKey.String("/session/validate"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), SessionValidateOperation,
@@ -10637,6 +11136,8 @@ func (s *Server) handleSetMapfixCompletedRequest(args [1]string, argsEscaped boo
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/completed"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), SetMapfixCompletedOperation,
@@ -10835,6 +11336,8 @@ func (s *Server) handleSetSubmissionCompletedRequest(args [1]string, argsEscaped
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/completed"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), SetSubmissionCompletedOperation,
@@ -11033,6 +11536,8 @@ func (s *Server) handleUpdateMapfixDescriptionRequest(args [1]string, argsEscape
semconv.HTTPRequestMethodKey.String("PATCH"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/description"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), UpdateMapfixDescriptionOperation,
@@ -11246,6 +11751,8 @@ func (s *Server) handleUpdateMapfixModelRequest(args [1]string, argsEscaped bool
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/model"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), UpdateMapfixModelOperation,
@@ -11452,6 +11959,8 @@ func (s *Server) handleUpdateScriptRequest(args [1]string, argsEscaped bool, w h
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/scripts/{ScriptID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), UpdateScriptOperation,
@@ -11665,6 +12174,8 @@ func (s *Server) handleUpdateScriptPolicyRequest(args [1]string, argsEscaped boo
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/script-policy/{ScriptPolicyID}"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), UpdateScriptPolicyOperation,
@@ -11878,6 +12389,8 @@ func (s *Server) handleUpdateSubmissionModelRequest(args [1]string, argsEscaped
semconv.HTTPRequestMethodKey.String("POST"),
semconv.HTTPRouteKey.String("/submissions/{SubmissionID}/model"),
}
// Add attributes from config.
otelAttrs = append(otelAttrs, s.cfg.Attributes...)
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), UpdateSubmissionModelOperation,

View File

@@ -33,6 +33,7 @@ const (
BatchAssetThumbnailsOperation OperationName = "BatchAssetThumbnails"
BatchUserThumbnailsOperation OperationName = "BatchUserThumbnails"
BatchUsernamesOperation OperationName = "BatchUsernames"
CombobulateMapOperation OperationName = "CombobulateMap"
CreateMapfixOperation OperationName = "CreateMapfix"
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
CreateScriptOperation OperationName = "CreateScript"
@@ -60,6 +61,7 @@ const (
ListSubmissionAuditEventsOperation OperationName = "ListSubmissionAuditEvents"
ListSubmissionsOperation OperationName = "ListSubmissions"
ReleaseSubmissionsOperation OperationName = "ReleaseSubmissions"
SeedCombobulatorOperation OperationName = "SeedCombobulator"
SessionRolesOperation OperationName = "SessionRoles"
SessionUserOperation OperationName = "SessionUser"
SessionValidateOperation OperationName = "SessionValidate"

View File

@@ -2030,6 +2030,89 @@ 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.

View File

@@ -1733,6 +1733,66 @@ 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:
@@ -4392,6 +4452,66 @@ 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:

View File

@@ -225,6 +225,13 @@ 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)
@@ -340,6 +347,7 @@ func encodeDownloadMapAssetResponse(response DownloadMapAssetOK, w http.Response
}
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())
@@ -464,6 +472,7 @@ 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())
@@ -621,6 +630,13 @@ 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)

View File

@@ -10,6 +10,51 @@ import (
"github.com/ogen-go/ogen/uri"
)
var (
rn46AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn48AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn85AllowedHeaders = map[string]string{
"PATCH": "Content-Type",
}
rn75AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn52AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn60AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn50AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn58AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn53AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn54AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn56AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn38AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn40AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
rn41AllowedHeaders = map[string]string{
"POST": "Content-Type",
}
)
func (s *Server) cutPrefix(path string) (string, bool) {
prefix := s.cfg.Prefix
if prefix == "" {
@@ -88,7 +133,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleCreateMapfixRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET,POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET,POST",
allowedHeaders: rn46AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -118,7 +168,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -152,7 +207,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -186,7 +246,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: rn48AllowedHeaders,
acceptPost: "text/plain",
acceptPatch: "",
})
}
return
@@ -208,7 +273,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -232,7 +302,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "PATCH")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "PATCH",
allowedHeaders: rn85AllowedHeaders,
acceptPost: "",
acceptPatch: "text/plain",
})
}
return
@@ -254,7 +329,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -300,7 +380,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -322,7 +407,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -356,7 +446,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -378,7 +473,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -400,7 +500,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -422,7 +527,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -446,7 +556,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -468,7 +583,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -504,7 +624,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -525,7 +650,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -547,7 +677,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -571,7 +706,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -593,7 +733,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -622,12 +767,42 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "GET":
s.handleListMapsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
}
switch elem[0] {
case '-': // Prefix: "-admin/seed-combobulator"
if l := len("-admin/seed-combobulator"); len(elem) >= l && elem[0:l] == "-admin/seed-combobulator" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "POST":
s.handleSeedCombobulatorRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
}
case '/': // Prefix: "/"
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
@@ -652,32 +827,83 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
}
switch elem[0] {
case '/': // Prefix: "/download"
case '/': // Prefix: "/"
if l := len("/download"); len(elem) >= l && elem[0:l] == "/download" {
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "GET":
s.handleDownloadMapAssetRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
break
}
switch elem[0] {
case 'c': // Prefix: "combobulate"
if l := len("combobulate"); len(elem) >= l && elem[0:l] == "combobulate" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "POST":
s.handleCombobulateMapRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
}
case 'd': // Prefix: "download"
if l := len("download"); len(elem) >= l && elem[0:l] == "download" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "GET":
s.handleDownloadMapAssetRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
}
return
}
}
@@ -711,7 +937,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -731,7 +962,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleReleaseSubmissionsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: rn75AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -776,7 +1012,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleCreateScriptPolicyRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET,POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET,POST",
allowedHeaders: rn52AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -815,7 +1056,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "DELETE,GET,POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "DELETE,GET,POST",
allowedHeaders: rn60AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -838,7 +1084,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleCreateScriptRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET,POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET,POST",
allowedHeaders: rn50AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -877,7 +1128,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "DELETE,GET,POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "DELETE,GET,POST",
allowedHeaders: rn58AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -913,7 +1169,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "GET":
s.handleSessionRolesRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -933,7 +1194,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "GET":
s.handleSessionUserRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -953,7 +1219,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "GET":
s.handleSessionValidateRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -975,7 +1246,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "GET":
s.handleGetStatsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -996,7 +1272,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleCreateSubmissionRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET,POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET,POST",
allowedHeaders: rn53AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -1016,7 +1297,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleCreateSubmissionAdminRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: rn54AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -1046,7 +1332,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1080,7 +1371,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1114,7 +1410,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: rn56AllowedHeaders,
acceptPost: "text/plain",
acceptPatch: "",
})
}
return
@@ -1136,7 +1437,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1160,7 +1466,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1206,7 +1517,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1228,7 +1544,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1262,7 +1583,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1284,7 +1610,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1306,7 +1637,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1330,7 +1666,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1352,7 +1693,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1387,7 +1733,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1409,7 +1760,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1433,7 +1789,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1455,7 +1816,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1522,7 +1888,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1542,7 +1913,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleBatchAssetThumbnailsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: rn38AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -1587,7 +1963,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "GET")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
@@ -1607,7 +1988,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleBatchUserThumbnailsRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: rn40AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -1631,7 +2017,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "POST":
s.handleBatchUsernamesRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "POST")
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: rn41AllowedHeaders,
acceptPost: "application/json",
acceptPatch: "",
})
}
return
@@ -2378,6 +2769,31 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
}
}
switch elem[0] {
case '-': // Prefix: "-admin/seed-combobulator"
if l := len("-admin/seed-combobulator"); len(elem) >= l && elem[0:l] == "-admin/seed-combobulator" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "POST":
r.name = SeedCombobulatorOperation
r.summary = "Queue all maps for combobulator processing"
r.operationID = "seedCombobulator"
r.operationGroup = ""
r.pathPattern = "/maps-admin/seed-combobulator"
r.args = args
r.count = 0
return r, true
default:
return
}
}
case '/': // Prefix: "/"
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
@@ -2411,29 +2827,68 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
}
}
switch elem[0] {
case '/': // Prefix: "/download"
case '/': // Prefix: "/"
if l := len("/download"); len(elem) >= l && elem[0:l] == "/download" {
if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "GET":
r.name = DownloadMapAssetOperation
r.summary = "Download the map asset"
r.operationID = "downloadMapAsset"
r.operationGroup = ""
r.pathPattern = "/maps/{MapID}/download"
r.args = args
r.count = 1
return r, true
default:
return
break
}
switch elem[0] {
case 'c': // Prefix: "combobulate"
if l := len("combobulate"); len(elem) >= l && elem[0:l] == "combobulate" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "POST":
r.name = CombobulateMapOperation
r.summary = "Queue a map for combobulator processing"
r.operationID = "combobulateMap"
r.operationGroup = ""
r.pathPattern = "/maps/{MapID}/combobulate"
r.args = args
r.count = 1
return r, true
default:
return
}
}
case 'd': // Prefix: "download"
if l := len("download"); len(elem) >= l && elem[0:l] == "download" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "GET":
r.name = DownloadMapAssetOperation
r.summary = "Download the map asset"
r.operationID = "downloadMapAsset"
r.operationGroup = ""
r.pathPattern = "/maps/{MapID}/download"
r.args = args
r.count = 1
return r, true
default:
return
}
}
}
}

View File

@@ -441,6 +441,9 @@ 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
@@ -1993,6 +1996,9 @@ 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{}

View File

@@ -32,6 +32,7 @@ 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{},
@@ -57,6 +58,7 @@ var operationRolesCookieAuth = map[string][]string{
ActionSubmissionTriggerUploadOperation: []string{},
ActionSubmissionTriggerValidateOperation: []string{},
ActionSubmissionValidatedOperation: []string{},
CombobulateMapOperation: []string{},
CreateMapfixOperation: []string{},
CreateMapfixAuditCommentOperation: []string{},
CreateScriptOperation: []string{},
@@ -69,6 +71,7 @@ var operationRolesCookieAuth = map[string][]string{
DownloadMapAssetOperation: []string{},
GetOperationOperation: []string{},
ReleaseSubmissionsOperation: []string{},
SeedCombobulatorOperation: []string{},
SessionRolesOperation: []string{},
SessionUserOperation: []string{},
SessionValidateOperation: []string{},
@@ -81,6 +84,27 @@ var operationRolesCookieAuth = map[string][]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
}
func (s *Server) securityCookieAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
var t CookieAuth
const parameterName = "session_id"

View File

@@ -173,6 +173,12 @@ 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.
@@ -335,6 +341,12 @@ 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.

View File

@@ -259,6 +259,15 @@ 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.
@@ -502,6 +511,15 @@ 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.

View File

@@ -19,6 +19,8 @@ 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"
@@ -123,6 +125,12 @@ 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,
},
},
}
}
@@ -144,7 +152,7 @@ func serve(ctx *cli.Context) error {
_, err = js.AddStream(&nats.StreamConfig{
Name: "maptest",
Subjects: []string{"maptest.>"},
Retention: nats.WorkQueuePolicy,
Retention: nats.InterestPolicy,
})
if err != nil {
log.WithError(err).Fatal("failed to add stream")
@@ -168,6 +176,13 @@ 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 {
@@ -180,6 +195,8 @@ func serve(ctx *cli.Context) error {
users.NewUsersServiceClient(conn),
robloxClient,
redisClient,
s3Client,
ctx.String("s3-bucket"),
)
svc_external := web_api.NewService(

View File

@@ -195,3 +195,13 @@ 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

@@ -47,6 +47,7 @@ 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
}

View File

@@ -23,6 +23,14 @@ 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

@@ -74,9 +74,21 @@ 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
if err := env.db.Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&events).Error; err != nil {
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 {
return nil, err
}

View File

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

View File

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

View File

@@ -81,6 +81,47 @@ 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,6 +93,13 @@ 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) {

View File

@@ -99,6 +99,10 @@ 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)
}

21
pkg/service/nats_maps.go Normal file
View File

@@ -0,0 +1,21 @@
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,11 +2,14 @@ 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"
)
@@ -17,6 +20,8 @@ type Service struct {
maps maps.MapsServiceClient
users users.UsersServiceClient
thumbnailService *ThumbnailService
s3Presign *s3.PresignClient
s3Bucket string
}
func NewService(
@@ -26,6 +31,8 @@ func NewService(
users users.UsersServiceClient,
robloxClient *roblox.Client,
redisClient *redis.Client,
s3Client *s3.Client,
s3Bucket string,
) Service {
return Service{
db: db,
@@ -33,9 +40,23 @@ 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

@@ -2,7 +2,6 @@ package web_api
import (
"context"
"errors"
"fmt"
"io"
"time"
@@ -35,10 +34,10 @@ var(
)
var (
ErrCreationPhaseMapfixesLimit = errors.New("Active mapfixes limited to 20")
ErrActiveMapfixSameTargetAssetID = errors.New("There is an active mapfix for this map already")
ErrCreationPhaseMapfixesLimit = fmt.Errorf("%w: Active mapfixes limited to 20", ErrPermissionDenied)
ErrActiveMapfixSameTargetAssetID = fmt.Errorf("%w: There is an active mapfix for this map already", ErrPermissionDenied)
ErrAcceptOwnMapfix = fmt.Errorf("%w: You cannot accept your own mapfix as the submitter", ErrPermissionDenied)
ErrCreateMapfixRateLimit = errors.New("You must not create more than 5 mapfixes every 10 minutes")
ErrCreateMapfixRateLimit = fmt.Errorf("%w: You must not create more than 5 mapfixes every 10 minutes", ErrTooManyRequests)
)
// POST /mapfixes

View File

@@ -86,6 +86,61 @@ 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"
"errors"
"fmt"
"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 = errors.New("SessionID missing")
ErrMissingSessionID = fmt.Errorf("%w: SessionID missing", ErrUserInfo)
// ErrInvalidSession caller does not have a valid session
ErrInvalidSession = errors.New("Session invalid")
ErrInvalidSession = fmt.Errorf("%w: Session invalid", ErrUserInfo)
)
type UserInfoHandle struct {

View File

@@ -12,6 +12,8 @@ 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
@@ -26,7 +28,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 = errors.New("A negative ID was provided")
ErrNegativeID = fmt.Errorf("%w: A negative ID was provided", ErrBadRequest)
)
type Service struct {
@@ -49,14 +51,20 @@ func NewService(
// Used for common default response.
func (svc *Service) NewError(ctx context.Context, err error) *api.ErrorStatusCode {
status := 500
if errors.Is(err, datastore.ErrNotExist) {
status = 404
if errors.Is(err, ErrBadRequest) {
status = 400
}
if errors.Is(err, ErrUserInfo) {
status = 401
}
if errors.Is(err, ErrPermissionDenied) {
status = 403
}
if errors.Is(err, ErrUserInfo) {
status = 401
if errors.Is(err, datastore.ErrNotExist) {
status = 404
}
if errors.Is(err, ErrTooManyRequests) {
status = 429
}
return &api.ErrorStatusCode{
StatusCode: status,

View File

@@ -2,7 +2,6 @@ package web_api
import (
"context"
"errors"
"fmt"
"io"
"time"
@@ -13,34 +12,35 @@ 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 = 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")
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)
)
// 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,13 +110,14 @@ 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 {
@@ -134,7 +135,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
}
@@ -155,8 +156,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
@@ -191,18 +192,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
}
@@ -214,28 +215,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)})
}
@@ -244,27 +245,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),
})
}
@@ -341,9 +342,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,
}
@@ -351,7 +352,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,
@@ -401,7 +402,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,
@@ -456,7 +457,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,
@@ -509,7 +510,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,
@@ -552,6 +553,28 @@ 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()
@@ -583,7 +606,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,
@@ -654,7 +677,7 @@ func (svc *Service) ActionSubmissionTriggerSubmitUnchecked(ctx context.Context,
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -682,7 +705,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
}
@@ -711,7 +734,7 @@ func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -779,7 +802,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,
@@ -816,7 +839,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
}
@@ -839,7 +862,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,
@@ -911,7 +934,7 @@ func (svc *Service) ActionSubmissionTriggerValidate(ctx context.Context, params
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -971,7 +994,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,
@@ -1008,7 +1031,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
}
@@ -1031,7 +1054,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,
@@ -1078,11 +1101,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
@@ -1108,8 +1131,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
@@ -1131,10 +1154,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
@@ -1175,7 +1198,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,
@@ -1191,7 +1214,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 = { version = "1", features = ["derive"] }
serde_json = "1"
serde.workspace = true
serde_json.workspace = true
serde_repr = "0.1.19"
url = "2"

View File

@@ -4,18 +4,18 @@ version = "0.1.1"
edition = "2024"
[dependencies]
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"
async-nats.workspace = true
futures-util.workspace = true
rbx_asset.workspace = true
rbx_binary.workspace = true
rbx_dom_weak.workspace = true
rbx_reflection_database = "2.0.1"
rbx_xml = "2.0.0"
regex = { version = "1.11.3", default-features = false }
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
serde.workspace = true
serde_json.workspace = true
siphasher = "1.0.1"
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
tokio.workspace = true
heck = "0.5.0"
rust-grpc = { version = "1.6.1", registry = "strafesnet" }
tonic = "0.14.1"

View File

@@ -1,3 +1,4 @@
FROM alpine:3.21 AS runtime
COPY /target/x86_64-unknown-linux-musl/release/maps-validation /
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 /
ENTRYPOINT ["/maps-validation"]

View File

@@ -324,25 +324,24 @@ 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 StringCheck<'a,T,Str>(Result<T,StringCheckContext<'a,Str>>);
pub struct StringCheckContext<'a,Str>{
pub struct StringEquality<'a,Str>{
observed:&'a str,
expected:Str,
}
impl<'a,Str> StringCheckContext<'a,Str>
impl<'a,Str> StringEquality<'a,Str>
where
&'a str:PartialEq<Str>,
{
/// Compute the StringCheck, passing through the provided value on success.
fn check<T>(self,value:T)->StringCheck<'a,T,Str>{
fn check<T>(self,value:T)->Result<T,Self>{
if self.observed==self.expected{
StringCheck(Ok(value))
Ok(value)
}else{
StringCheck(Err(self))
Err(self)
}
}
}
impl<Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'_,Str>{
impl<Str:std::fmt::Display> std::fmt::Display for StringEquality<'_,Str>{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"expected: {}, observed: {}",self.expected,self.observed)
}
@@ -464,19 +463,66 @@ 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:StringCheck<'a,(),&'static str>,
model_class:Result<(),StringEquality<'a,&'static str>>,
// Model's name must be in snake case
model_name:StringCheck<'a,(),String>,
model_name:Result<(),StringEquality<'a,String>>,
// Map must have a StringValue named DisplayName.
// Value must not be empty, must be in title case.
display_name:Result<Result<StringCheck<'a,&'a str,String>,StringEmpty>,StringValueError>,
display_name:Result<&'a str,DisplayNameError<'a>>,
// Map must have a StringValue named Creator.
// Value must not be empty.
creator:Result<Result<&'a str,StringEmpty>,StringValueError>,
creator:Result<&'a str,CreatorError>,
// 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>,
@@ -511,27 +557,22 @@ struct MapCheck<'a>{
impl<'a> ModelInfo<'a>{
fn check(self)->MapCheck<'a>{
// Check class is exactly "Model"
let model_class=StringCheckContext{
let model_class=StringEquality{
observed:self.model_class,
expected:"Model",
}.check(());
// Check model name is snake case
let model_name=StringCheckContext{
let model_name=StringEquality{
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=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))
});
let display_name=check_display_name(self.map_info.display_name);
// Check Creator is not empty
let creator=self.map_info.creator.map(check_empty);
let creator=check_creator(self.map_info.creator);
// Check GameID (model name was prefixed with bhop_ surf_ etc)
let game_id=self.map_info.game_id;
@@ -630,10 +671,10 @@ impl MapCheck<'_>{
fn result(self)->Result<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
match self{
MapCheck{
model_class:StringCheck(Ok(())),
model_name:StringCheck(Ok(())),
display_name:Ok(Ok(StringCheck(Ok(display_name)))),
creator:Ok(Ok(creator)),
model_class:Ok(()),
model_name:Ok(()),
display_name:Ok(display_name),
creator:Ok(creator),
game_id:Ok(game_id),
mapstart:Ok(Exists),
mode_start_counts:DuplicateCheck(Ok(())),
@@ -737,27 +778,25 @@ macro_rules! summary_format{
impl MapCheck<'_>{
fn itemize(&self)->Result<MapCheckList,serde_json::Error>{
let model_class=match &self.model_class{
StringCheck(Ok(()))=>passed!("ModelClass"),
StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}"),
Ok(())=>passed!("ModelClass"),
Err(context)=>summary_format!("ModelClass","Invalid model class: {context}"),
};
let model_name=match &self.model_name{
StringCheck(Ok(()))=>passed!("ModelName"),
StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}"),
Ok(())=>passed!("ModelName"),
Err(context)=>summary_format!("ModelName","Model name must have snake_case: {context}"),
};
let display_name=match &self.display_name{
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()),
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}"),
};
let creator=match &self.creator{
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()),
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}"),
};
let game_id=match &self.game_id{
Ok(_)=>passed!("GameID"),

View File

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

View File

@@ -79,6 +79,15 @@ 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,4 +1,5 @@
use futures::StreamExt;
use futures_util::stream::iter as stream_iter;
use futures_util::StreamExt;
use crate::download::download_asset_version;
use crate::nats_types::ReleaseSubmissionsBatchRequest;
@@ -92,7 +93,7 @@ async fn release_inner(
.collect();
// fut_download
let fut_download=futures::stream::iter(asset_versions)
let fut_download=stream_iter(asset_versions)
.map(|(index,asset_version)|async move{
let modes=download_fut(cloud_context,asset_version).await;
(index,modes)
@@ -137,7 +138,7 @@ async fn release_inner(
}
// concurrently dispatch results
let release_results:Vec<_> =futures::stream::iter(
let release_results:Vec<_> =stream_iter(
release_info
.Submissions
.into_iter()

View File

@@ -1,4 +1,5 @@
use futures::TryStreamExt;
use futures_util::stream::iter as stream_iter;
use futures_util::TryStreamExt;
use rust_grpc::validator::Policy;
use crate::download::download_asset_version;
@@ -153,7 +154,7 @@ impl crate::message_handler::MessageHandler{
}
// send all script hashes to REST endpoint and retrieve the replacements
futures::stream::iter(script_map.iter_mut().map(Ok))
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,7 +4,14 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Maps Service</title>
<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>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
import { Routes, Route } from 'react-router-dom'
import { ThemeProvider } from '@mui/material'
import { ThemeProvider, CssBaseline } from '@mui/material'
import { theme } from '@/app/lib/theme'
// Pages
@@ -22,6 +22,7 @@ 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

@@ -0,0 +1,34 @@
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(99, 102, 241, 0.1)',
backgroundColor: 'rgba(167, 139, 250, 0.08)',
},
}}
>
@@ -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(99, 102, 241, 0.2)',
border: '1px solid rgba(167, 139, 250, 0.15)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
'&:hover': {
backgroundColor: 'background.paper',
borderColor: 'rgba(99, 102, 241, 0.4)',
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
borderColor: 'rgba(167, 139, 250, 0.3)',
boxShadow: '0 8px 20px rgba(167, 139, 250, 0.2)',
},
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(99, 102, 241, 0.2)',
border: '1px solid rgba(167, 139, 250, 0.15)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
'&:hover': {
backgroundColor: 'background.paper',
borderColor: 'rgba(99, 102, 241, 0.4)',
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
borderColor: 'rgba(167, 139, 250, 0.3)',
boxShadow: '0 8px 20px rgba(167, 139, 250, 0.2)',
},
visibility: scrollPosition >= maxScroll - 5 ? 'hidden' : 'visible',
}}

View File

@@ -6,6 +6,7 @@ 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";
@@ -75,7 +76,7 @@ export default function CommentsAndAuditSection({
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#ff9800',
backgroundColor: semantic.warning,
animation: `${pulse} 2s ease-in-out infinite`
}}
/>

View File

@@ -1,6 +1,7 @@
import { Link } from "react-router-dom"
import { useState, useRef } from "react";
import { useState } 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";
@@ -10,51 +11,44 @@ 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';
interface HeaderButton {
name: string;
href: string;
}
const navItems: HeaderButton[] = [
const navItems = [
{ name: "Home", href: "/" },
{ name: "Submissions", href: "/submissions" },
{ name: "Mapfixes", href: "/mapfixes" },
{ name: "Maps", href: "/maps" },
];
function HeaderButton(header: HeaderButton) {
return (
<Button color="inherit" component={Link} to={header.href}>
{header.name}
</Button>
);
}
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" },
];
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';
};
@@ -86,9 +80,20 @@ export default function Header() {
setQuickLinksAnchor(null);
};
// Mobile navigation drawer content
const drawer = (
<Box onClick={handleDrawerToggle} sx={{ textAlign: 'center' }}>
<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>
<List>
{navItems.map((item) => (
<ListItem key={item.name} disablePadding>
@@ -100,7 +105,7 @@ export default function Header() {
{isLoggedIn && user && (
<ListItem disablePadding>
<ListItemButton component={Link} to="/submit" sx={{ textAlign: 'center' }}>
<ListItemText primary="Submit Map" sx={{ color: 'success.main' }} />
<ListItemText primary="Submit Map" sx={{ color: primary.main }} />
</ListItemButton>
</ListItem>
)}
@@ -122,125 +127,88 @@ 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="static">
<Toolbar sx={{ py: 1 }}>
<AppBar position="sticky">
<Toolbar sx={{ gap: 1 }}>
{isMobile && (
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2 }}
sx={{ color: primary.main }}
>
<MenuIcon />
</IconButton>
)}
{/* Desktop navigation */}
{/* Brand */}
{!isMobile && (
<Box display="flex" flexGrow={1} gap={1} alignItems="center">
{/* Logo/Brand */}
<Box
component={Link}
to="/"
<Box
component={Link}
to="/"
sx={{
mr: 4,
textDecoration: 'none',
display: 'flex',
alignItems: 'baseline',
gap: 1,
}}
>
<Typography
variant="h6"
sx={{
mr: 4,
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
position: 'relative',
overflow: 'hidden',
'@keyframes speedLine': {
'0%': {
transform: 'translateX(-50px) scaleX(0.5)',
opacity: 0,
},
'40%': {
opacity: 0.8,
transform: 'translateX(0px) scaleX(1)',
},
'100%': {
opacity: 0,
transform: 'translateX(30px) scaleX(0.7)',
},
},
'@keyframes logoReveal': {
'0%': {
opacity: 0,
transform: 'translateX(-10px)',
filter: 'blur(2px)',
},
'100%': {
opacity: 1,
transform: 'translateX(0px)',
filter: 'blur(0px)',
},
},
'&::before, &::after': {
content: '""',
position: 'absolute',
left: 0,
width: '100%',
height: '2px',
background: 'linear-gradient(90deg, transparent 10%, rgba(59, 130, 246, 0.8) 50%, transparent 90%)',
pointerEvents: 'none',
animation: !hasAnimated.current ? 'speedLine 0.6s ease-out forwards' : 'none',
opacity: !hasAnimated.current ? 0 : undefined,
},
'&::before': {
top: '35%',
animationDelay: !hasAnimated.current ? '0s' : undefined,
},
'&::after': {
top: '65%',
animationDelay: !hasAnimated.current ? '0.08s' : undefined,
},
fontFamily: '"Barlow", sans-serif',
fontWeight: 700,
letterSpacing: '-0.02em',
color: text.primary,
lineHeight: 1,
userSelect: 'none',
}}
>
<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>
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 && (
<>
{navItems.map((item) => (
<Button
key={item.name}
@@ -248,192 +216,157 @@ export default function Header() {
component={Link}
to={item.href}
sx={{
px: 2,
py: 1,
borderRadius: 1.5,
fontSize: '0.9rem',
px: 1.5,
py: 0.75,
borderRadius: 2,
fontSize: '0.85rem',
fontWeight: 500,
color: 'text.secondary',
transition: 'all 0.2s',
color: text.tertiary,
transition: 'all 0.15s ease',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.08)',
color: 'text.primary',
backgroundColor: fill.primaryHover,
color: text.primary,
},
}}
>
{item.name}
</Button>
))}
<Box sx={{ flexGrow: 1 }} />
{/* Quick Links Dropdown */}
<Box>
<Button
color="inherit"
endIcon={<ArrowDropDownIcon />}
onClick={handleQuickLinksOpen}
sx={{
px: 2,
mr: 1,
borderRadius: 1.5,
fontSize: '0.9rem',
fontWeight: 500,
color: 'text.secondary',
transition: 'all 0.2s',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.08)',
color: 'text.primary',
},
}}
>
Quick Links
</Button>
<Menu
anchorEl={quickLinksAnchor}
open={Boolean(quickLinksAnchor)}
onClose={handleQuickLinksClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{
'& .MuiMenu-paper': {
mt: 1.5,
},
}}
>
{quickLinks.map(link => (
<MenuItem
key={link.name}
onClick={handleQuickLinksClose}
sx={{
minWidth: 200,
fontSize: '0.9rem',
}}
component="a"
href={link.href}
target="_blank"
rel="noopener noreferrer"
>
{link.name}
</MenuItem>
))}
</Menu>
</Box>
</Box>
</>
)}
{/* Spacer for mobile view */}
{isMobile && <Box sx={{ flexGrow: 1 }} />}
<Box sx={{ flex: 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)',
},
}}
{/* 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"
>
<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>
)}
{link.name}
</MenuItem>
))}
</Menu>
</Box>
)}
{/* In mobile view, display just the avatar if logged in */}
{isMobile && isLoggedIn && user && (
{/* 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}
color="inherit"
size="small"
sx={{
ml: 0.5,
p: 0.5,
transition: 'all 0.15s ease',
'&:hover': {
boxShadow: '0 0 12px rgba(167, 139, 250, 0.3)',
},
}}
>
<img
className="avatar"
width={32}
height={32}
src={user.AvatarURL}
alt={user.Username}
style={{
borderRadius: '50%',
}}
/>
<Avatar
src={user.AvatarURL}
sx={{
width: 32,
height: 32,
fontSize: '0.75rem',
fontWeight: 700,
backgroundColor: primary.dark,
color: text.primary,
}}
>
{user.Username?.slice(0, 2).toUpperCase()}
</Avatar>
</IconButton>
)}
</Box>
<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>
</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>
)}
</Toolbar>
{/* Mobile drawer */}
@@ -441,9 +374,7 @@ export default function Header() {
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
ModalProps={{ keepMounted: true }}
sx={{
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
@@ -455,4 +386,4 @@ export default function Header() {
</Drawer>
</AppBar>
);
}
}

View File

@@ -5,6 +5,7 @@ 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;
@@ -119,13 +120,13 @@ export function MapCard(props: MapCardProps) {
flexWrap: 'wrap',
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Explore sx={{ fontSize: '1rem', color: '#6366f1' }} />
<Explore sx={{ fontSize: '1rem', color: gameColors[props.gameID] || primary.main }} />
<Typography variant="body2" color="text.secondary" fontSize="0.875rem">
{getGameName(props.gameID)}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Person2 sx={{ fontSize: '1rem', color: '#8b5cf6' }} />
<Person2 sx={{ fontSize: '1rem', color: primary.main }} />
{props.type === 'mapfix' && usernameLoading ? (
<Skeleton variant="text" width={80} />
) : (

View File

@@ -6,6 +6,7 @@ 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% {
@@ -188,8 +189,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: '#2196f3',
bgColor: 'rgba(33, 150, 243, 0.08)'
color: semantic.info,
bgColor: 'rgba(56, 189, 248, 0.08)'
};
}
if (isChangesRequested) {
@@ -197,8 +198,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: '#ff9800',
bgColor: 'rgba(255, 152, 0, 0.08)'
color: semantic.warning,
bgColor: 'rgba(251, 191, 36, 0.08)'
};
}
return null;

View File

@@ -1,86 +1,76 @@
import {JSX} from "react";
import {Cancel, CheckCircle, Pending} from "@mui/icons-material";
import {Chip} from "@mui/material";
import { semantic, text } from "@/app/lib/colors";
export const StatusChip = ({status}: { status: number }): JSX.Element => {
let color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
let icon: JSX.Element = <Pending fontSize="small"/>;
let label: string = 'Unknown';
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:
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;
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);
return (
<Chip
icon={icon}
label={label}
color={color}
icon={config.icon}
label={config.label}
size="small"
sx={{
backgroundColor: config.bg,
color: config.color,
border: `1px solid ${config.border}`,
'& .MuiChip-icon': {
color: config.color,
},
}}
/>
);
};

View File

@@ -1,8 +1,13 @@
import { Box } from "@mui/material";
import Header from "./header";
import AnimatedBackground from "./AnimatedBackground";
export default function Webpage({children}: Readonly<{children?: React.ReactNode}>) {
return <>
<Header/>
{children}
<AnimatedBackground />
<Box sx={{ position: 'relative', zIndex: 1, minHeight: '100vh' }}>
<Header/>
{children}
</Box>
</>
}

View File

@@ -24,10 +24,10 @@
color: var(--text-color);
}
& fieldset {
border-color: rgb(100,100,100);
border-color: rgba(255, 255, 255, 0.1);
}
& span {
color: white;
color: #fafafa;
}
}

View File

@@ -1,37 +1,18 @@
$review-border: 1px solid var(--review-border);
$review-border: 1px solid rgba(255, 255, 255, 0.06);
$form-label-fontsize: 1.3rem;
@mixin border-with-radius {
border: $review-border {
radius: 5px;
radius: 8px;
}
}
:root {
color-scheme: dark;
--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);
--review-border: rgba(255, 255, 255, 0.06);
--text-color: #fafafa;
--placeholder-text: #52525b;
}
button {
@@ -41,10 +22,3 @@ button {
a:active, a:link, a:hover {
text-decoration: none;
}
.spacer {
display: block;
width: 100%;
height: 1px;
background-color: var(--review-border);
}

88
web/src/app/lib/colors.ts Normal file
View File

@@ -0,0 +1,88 @@
// 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,133 +1,263 @@
import {createTheme} from "@mui/material";
import { createTheme } from '@mui/material';
import { primary, secondary, semantic, surface, text, border, fill, gradients, glow } from './colors';
export const theme = createTheme({
cssVariables: {
colorSchemeSelector: 'class',
},
colorSchemes: {
dark: true,
},
defaultColorScheme: 'dark',
palette: {
mode: 'dark',
primary: {
main: '#3b82f6',
dark: '#2563eb',
light: '#60a5fa',
main: primary.main,
light: primary.light,
dark: primary.dark,
},
secondary: {
main: '#8b5cf6',
dark: '#7c3aed',
light: '#a78bfa',
main: secondary.main,
light: secondary.light,
dark: secondary.dark,
},
background: {
default: '#0a0a0a',
paper: '#171717',
default: surface.base,
paper: surface.raised,
},
error: { main: semantic.error },
warning: { main: semantic.warning },
success: { main: semantic.success },
info: { main: semantic.info },
text: {
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',
primary: text.primary,
secondary: text.tertiary,
},
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: '#171717',
border: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)',
backgroundColor: surface.raisedAlpha,
backdropFilter: 'blur(12px)',
border: `1px solid ${border.default}`,
boxShadow: 'none',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
transform: 'translateY(-4px)',
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)',
border: `1px solid ${border.primaryMedium}`,
boxShadow: glow.button,
},
},
},
},
MuiCardMedia: {
styleOverrides: {
root: {
transition: 'transform 0.3s',
},
},
},
MuiCardContent: {
styleOverrides: {
root: {
padding: 16,
'&:last-child': {
paddingBottom: 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 },
},
},
},
@@ -136,134 +266,39 @@ export const theme = createTheme({
styleOverrides: {
root: {
fontWeight: 600,
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',
},
borderRadius: 6,
},
},
},
MuiDivider: {
styleOverrides: {
root: {
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',
borderColor: border.default,
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: '#0a0a0a',
borderRight: '1px solid rgba(255, 255, 255, 0.08)',
backgroundColor: surface.base,
borderRight: `1px solid ${border.default}`,
},
},
},
MuiCircularProgress: {
styleOverrides: {
root: {
color: '#3b82f6',
color: primary.main,
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
transition: 'all 0.2s ease-in-out',
transition: 'all 0.15s ease',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
backgroundColor: fill.primaryActive,
},
},
},
@@ -271,11 +306,11 @@ export const theme = createTheme({
MuiLink: {
styleOverrides: {
root: {
color: '#60a5fa',
color: primary.light,
textDecoration: 'none',
transition: 'color 0.2s ease-in-out',
transition: 'color 0.15s ease',
'&:hover': {
color: '#3b82f6',
color: primary.main,
textDecoration: 'underline',
},
},
@@ -284,28 +319,28 @@ export const theme = createTheme({
MuiMenu: {
styleOverrides: {
paper: {
background: '#171717',
backdropFilter: 'blur(12px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
background: surface.raisedSolid,
backdropFilter: 'blur(16px)',
border: `1px solid ${border.default}`,
boxShadow: glow.palette,
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
transition: 'all 0.2s ease-in-out',
transition: 'all 0.15s ease',
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
backgroundColor: fill.primaryHover,
},
'&.Mui-selected': {
backgroundColor: 'rgba(59, 130, 246, 0.15)',
backgroundColor: fill.primaryActive,
'&:hover': {
backgroundColor: 'rgba(59, 130, 246, 0.2)',
backgroundColor: fill.primaryStrong,
},
},
},
},
},
},
});
});

View File

@@ -391,12 +391,12 @@ export default function MapDetails() {
px: 2,
borderRadius: 1,
transition: 'all 0.2s',
backgroundColor: 'rgba(25, 118, 210, 0.08)',
backgroundColor: 'rgba(167, 139, 250, 0.08)',
borderLeft: '4px solid',
borderColor: 'primary.main',
mb: releasedFixes.length > 0 ? 2 : 0,
'&:hover': {
backgroundColor: 'rgba(25, 118, 210, 0.12)',
backgroundColor: 'rgba(167, 139, 250, 0.12)',
transform: 'translateX(4px)'
},
textDecoration: 'none',

View File

@@ -4,6 +4,7 @@ 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");
@@ -19,7 +20,7 @@ export default function NotFound() {
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
background: 'linear-gradient(to bottom, #0a0a0a 0%, #0f0f0f 100%)',
background: `linear-gradient(to bottom, ${surface.base} 0%, #0f0f0f 100%)`,
}}
>
{/* Subtle Gradient Background */}
@@ -30,7 +31,7 @@ export default function NotFound() {
right: '30%',
width: '500px',
height: '500px',
background: 'radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%)',
background: `radial-gradient(circle, rgba(248, 113, 113, 0.1) 0%, transparent 70%)`,
borderRadius: '50%',
filter: 'blur(80px)',
}}
@@ -42,7 +43,7 @@ export default function NotFound() {
left: '25%',
width: '450px',
height: '450px',
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%)',
background: `radial-gradient(circle, rgba(167, 139, 250, 0.08) 0%, transparent 70%)`,
borderRadius: '50%',
filter: 'blur(80px)',
}}
@@ -59,7 +60,7 @@ export default function NotFound() {
lineHeight: 1,
mb: 2,
letterSpacing: '-0.04em',
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
background: `linear-gradient(135deg, ${semantic.error} 0%, #dc2626 100%)`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
@@ -173,7 +174,7 @@ export default function NotFound() {
fontSize: '1rem',
'&:hover': {
color: 'primary.main',
background: 'rgba(59, 130, 246, 0.1)',
background: `rgba(167, 139, 250, 0.1)`,
},
}}
>

View File

@@ -26,6 +26,7 @@ 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");
@@ -167,8 +168,8 @@ export default function Home() {
}}
>
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
<CircularProgress size={60} thickness={4} sx={{ color: 'primary.main' }}/>
<Typography variant="h6" color="text.secondary">
<CircularProgress size={48} thickness={3} />
<Typography variant="body2" sx={{ color: text.muted }}>
Loading content...
</Typography>
</Box>
@@ -223,32 +224,32 @@ export default function Home() {
value: totalSubmissions,
label: 'Total Submissions',
sublabel: 'Total maps submitted by the community',
color: '#3b82f6',
gradient: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
color: primary.main,
gradient: gradients.button,
},
{
icon: <BuildIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
value: totalMapfixes,
label: 'Total Map Fixes',
sublabel: 'Total map fixes submitted by the community',
color: '#8b5cf6',
gradient: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
color: secondary.main,
gradient: `linear-gradient(135deg, ${secondary.dark} 0%, ${secondary.main} 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: '#10b981',
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: semantic.success,
gradient: `linear-gradient(135deg, ${semantic.success} 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: '#10b981',
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: semantic.success,
gradient: `linear-gradient(135deg, ${semantic.success} 0%, #059669 100%)`,
},
{
icon: <EmojiEventsIcon sx={{ fontSize: { xs: 48, md: 64 } }} />,
@@ -263,8 +264,8 @@ export default function Home() {
value: submittedSubmissions + submittedMapfixes,
label: 'Under Review',
sublabel: 'Pending approval fixes & submissions',
color: '#f59e0b',
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
color: semantic.warning,
gradient: `linear-gradient(135deg, ${semantic.warning} 0%, #d97706 100%)`,
},
];
@@ -284,8 +285,8 @@ export default function Home() {
}}
>
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
<CircularProgress size={60} thickness={4} sx={{ color: 'primary.main' }}/>
<Typography variant="h6" color="text.secondary">
<CircularProgress size={48} thickness={3} />
<Typography variant="body2" sx={{ color: text.muted }}>
Loading...
</Typography>
</Box>
@@ -305,7 +306,7 @@ export default function Home() {
return (
<Webpage>
<Box sx={{ width: '100%', bgcolor: 'background.default' }}>
<Box sx={{ width: '100%' }}>
{/* Hero Section */}
<Box
sx={{
@@ -314,10 +315,9 @@ export default function Home() {
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
background: 'radial-gradient(ellipse at top, #0f1419 0%, #0a0a0a 50%, #000000 100%)',
}}
>
{/* Animated Background Elements */}
{/* Animated Background Orbs */}
<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(59, 130, 246, 0.15) 0%, transparent 70%)',
background: `radial-gradient(circle, rgba(124, 58, 237, 0.12) 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(139, 92, 246, 0.12) 0%, transparent 70%)',
background: `radial-gradient(circle, rgba(34, 211, 238, 0.08) 0%, transparent 70%)`,
borderRadius: '50%',
filter: 'blur(80px)',
animation: 'float-reverse 30s ease-in-out infinite',
'@keyframes float-reverse': {
animation: 'floatReverse 30s ease-in-out infinite',
'@keyframes floatReverse': {
'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(59, 130, 246, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(59, 130, 246, 0.03) 1px, transparent 1px)
linear-gradient(rgba(124, 58, 237, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(124, 58, 237, 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(59, 130, 246, 0.3) 50%, transparent 100%)',
background: `linear-gradient(90deg, transparent 0%, rgba(124, 58, 237, 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(139, 92, 246, 0.3) 50%, transparent 100%)',
background: `linear-gradient(90deg, transparent 0%, rgba(34, 211, 238, 0.2) 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: 'linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #c084fc 100%)',
background: gradients.brandText,
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.secondary',
color: text.tertiary,
mb: 5,
lineHeight: 1.75,
fontWeight: 400,
@@ -471,7 +471,7 @@ export default function Home() {
</Typography>
</Box>
{/* CTA Buttons - Moved up for better hierarchy */}
{/* CTA Buttons */}
<Box
display="flex"
gap={3}
@@ -493,13 +493,10 @@ export default function Home() {
px: { xs: 4, md: 5 },
py: { xs: 1.75, md: 2.25 },
fontWeight: 700,
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
boxShadow: '0 8px 32px rgba(59, 130, 246, 0.4)',
boxShadow: glow.button,
borderRadius: 2,
textTransform: 'none',
'&:hover': {
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
boxShadow: '0 12px 40px rgba(59, 130, 246, 0.6)',
boxShadow: glow.brandStrong,
transform: 'translateY(-2px)',
},
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
@@ -519,14 +516,9 @@ 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)',
@@ -536,7 +528,7 @@ export default function Home() {
</Button>
</Box>
{/* Stats Section - Completely Redesigned */}
{/* Stats Section */}
<Box
sx={{
animation: 'fadeIn 1.1s ease-out 0.4s both',
@@ -546,7 +538,6 @@ export default function Home() {
},
}}
>
{/* Stats Grid */}
<Box
sx={{
display: 'grid',
@@ -575,22 +566,13 @@ export default function Home() {
cursor: 'pointer',
background: currentStatIndex === index
? `linear-gradient(135deg, ${stat.color}15 0%, ${stat.color}08 100%)`
: 'rgba(17, 17, 17, 0.4)',
: fill.subtle,
backdropFilter: 'blur(10px)',
border: currentStatIndex === index
? `1px solid ${stat.color}40`
: '1px solid rgba(255, 255, 255, 0.05)',
: `1px solid ${border.subtle}`,
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%)`,
@@ -599,7 +581,6 @@ export default function Home() {
},
}}
>
{/* Icon */}
<Box
sx={{
display: 'flex',
@@ -623,13 +604,12 @@ 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,
@@ -638,11 +618,10 @@ export default function Home() {
{stat.value}
</Typography>
{/* Label */}
<Typography
variant="caption"
sx={{
color: currentStatIndex === index ? 'text.primary' : 'text.secondary',
color: currentStatIndex === index ? text.primary : text.tertiary,
fontSize: { xs: '0.7rem', md: '0.75rem' },
fontWeight: 600,
textAlign: 'center',
@@ -659,7 +638,6 @@ export default function Home() {
))}
</Box>
{/* Featured Stat Description */}
<Box
key={currentStatIndex}
sx={{
@@ -673,7 +651,7 @@ export default function Home() {
<Typography
variant="body1"
sx={{
color: 'text.secondary',
color: text.muted,
fontSize: { xs: '0.9rem', md: '1rem' },
fontWeight: 500,
maxWidth: '600px',
@@ -708,8 +686,7 @@ export default function Home() {
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{ maxWidth: '600px' }}
sx={{ color: text.muted, maxWidth: '600px' }}
>
Discover the newest custom maps created by the community
</Typography>
@@ -740,8 +717,7 @@ export default function Home() {
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{ maxWidth: '600px' }}
sx={{ color: text.muted, maxWidth: '600px' }}
>
Community-created map fixes and improvements
</Typography>
@@ -771,8 +747,7 @@ export default function Home() {
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{ maxWidth: '600px' }}
sx={{ color: text.muted, maxWidth: '600px' }}
>
Join the community and start contributing today
</Typography>
@@ -791,21 +766,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: '#3b82f6',
color: primary.main,
},
{
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: '#8b5cf6',
color: secondary.main,
},
{
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: '#10b981',
color: semantic.success,
},
].map((card, index) => (
<Box
@@ -814,16 +789,17 @@ export default function Home() {
to={card.link}
sx={{
p: 5,
background: 'rgba(23, 23, 23, 0.5)',
borderRadius: 2,
border: '1px solid rgba(255, 255, 255, 0.08)',
background: fill.subtle,
backdropFilter: 'blur(12px)',
borderRadius: 3,
border: `1px solid ${border.default}`,
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`,
},
@@ -835,8 +811,8 @@ export default function Home() {
sx={{
display: 'inline-flex',
p: 2,
borderRadius: 1.5,
background: `${card.color}20`,
borderRadius: 2,
background: `${card.color}15`,
mb: 3,
color: card.color,
transition: 'background 0.3s',
@@ -847,7 +823,7 @@ export default function Home() {
<Typography
variant="h5"
sx={{
fontWeight: 600,
fontWeight: 700,
mb: 1.5,
letterSpacing: '-0.01em',
}}
@@ -857,7 +833,7 @@ export default function Home() {
<Typography
variant="body2"
sx={{
color: 'text.secondary',
color: text.muted,
lineHeight: 1.7,
}}
>
@@ -874,8 +850,7 @@ export default function Home() {
sx={{
position: 'relative',
py: 12,
background: '#0f0f0f',
borderTop: '1px solid rgba(255, 255, 255, 0.08)',
borderTop: `1px solid ${border.default}`,
}}
>
<Container maxWidth="md" sx={{ position: 'relative', textAlign: 'center' }}>
@@ -893,7 +868,7 @@ export default function Home() {
<Typography
variant="body1"
sx={{
color: 'text.secondary',
color: text.muted,
mb: 5,
lineHeight: 1.7,
fontSize: '1.125rem',

View File

@@ -522,7 +522,10 @@ export default function ReviewerDashboardPage() {
mb: 4
}}>
{canReviewSubmissions && (
<Card onClick={()=>setTabValue(tabIndexSubmissions)}>
<Card
onClick={() => setTabValue(tabIndexSubmissions)}
sx={{ cursor: 'pointer' }}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<AssignmentIcon sx={{ fontSize: 40, color: 'primary.main' }} />
@@ -546,7 +549,10 @@ export default function ReviewerDashboardPage() {
)}
{canReviewMapfixes && (
<Card onClick={()=>setTabValue(tabIndexMapfixes)}>
<Card
onClick={() => setTabValue(tabIndexMapfixes)}
sx={{ cursor: 'pointer' }}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<BuildIcon sx={{ fontSize: 40, color: 'secondary.main' }} />
@@ -573,15 +579,7 @@ export default function ReviewerDashboardPage() {
<Card
component={Link}
to="/script-review"
sx={{
textDecoration: 'none',
cursor: 'pointer',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: 4
}
}}
sx={{ cursor: 'pointer' }}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>

View File

@@ -34,6 +34,7 @@ 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;
@@ -61,43 +62,43 @@ const IDEButton = ({
switch (variant) {
case 'primary':
return {
bg: '#0e639c',
hoverBg: '#1177bb',
activeBg: '#007acc',
bg: primary.dark,
hoverBg: primary.mid,
activeBg: primary.main,
color: '#ffffff',
border: '#007acc',
border: primary.main,
};
case 'success':
return {
bg: '#0e7e0e',
hoverBg: '#0f9d0f',
activeBg: '#14b814',
bg: '#166534',
hoverBg: '#15803d',
activeBg: semantic.success,
color: '#ffffff',
border: '#14b814',
border: semantic.success,
};
case 'error':
return {
bg: '#7e0e0e',
hoverBg: '#9d0f0f',
activeBg: '#b81414',
bg: '#7f1d1d',
hoverBg: '#991b1b',
activeBg: semantic.error,
color: '#ffffff',
border: '#b81414',
border: semantic.error,
};
case 'warning':
return {
bg: '#7e5e0e',
hoverBg: '#9d750f',
activeBg: '#b88614',
bg: '#78350f',
hoverBg: '#92400e',
activeBg: semantic.warning,
color: '#ffffff',
border: '#b88614',
border: semantic.warning,
};
default:
return {
bg: 'transparent',
hoverBg: 'rgba(255, 255, 255, 0.08)',
activeBg: 'rgba(255, 255, 255, 0.12)',
color: '#cccccc',
border: '#3e3e42',
color: text.secondary,
border: text.dim,
};
}
};
@@ -163,13 +164,13 @@ const InfoBadge = ({
const getColors = () => {
switch (type) {
case 'warning':
return { bg: 'rgba(250, 200, 90, 0.15)', border: '#fac85a', color: '#fac85a' };
return { bg: `rgba(251, 191, 36, 0.15)`, border: semantic.warning, color: semantic.warning };
case 'error':
return { bg: 'rgba(240, 82, 82, 0.15)', border: '#f05252', color: '#f05252' };
return { bg: `rgba(248, 113, 113, 0.15)`, border: semantic.error, color: semantic.error };
case 'success':
return { bg: 'rgba(80, 200, 120, 0.15)', border: '#50c878', color: '#50c878' };
return { bg: `rgba(74, 222, 128, 0.15)`, border: semantic.success, color: semantic.success };
default:
return { bg: 'rgba(100, 150, 230, 0.15)', border: '#6496e6', color: '#6496e6' };
return { bg: `rgba(167, 139, 250, 0.15)`, border: primary.main, color: primary.main };
}
};
@@ -636,15 +637,15 @@ export default function ScriptReviewPage() {
<Box sx={{
width: '100vw',
height: '100vh',
bgcolor: '#1e1e1e',
bgcolor: surface.raised,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#cccccc',
color: text.secondary,
}}>
<Box sx={{ textAlign: 'center' }}>
<LinearProgress sx={{ mb: 2, width: 300 }} />
<Typography sx={{ color: '#cccccc', fontSize: '14px' }}>Loading script...</Typography>
<Typography sx={{ color: text.secondary, fontSize: '14px' }}>Loading script...</Typography>
</Box>
</Box>
);
@@ -703,13 +704,13 @@ export default function ScriptReviewPage() {
height: '100vh',
display: 'flex',
flexDirection: 'column',
bgcolor: '#1e1e1e',
bgcolor: surface.raised,
overflow: 'hidden',
}}>
{/* Title Bar */}
<Box sx={{
bgcolor: '#323233',
borderBottom: '1px solid #2b2b2c',
bgcolor: '#27272a',
borderBottom: `1px solid ${border.default}`,
display: 'flex',
alignItems: 'center',
px: 2,
@@ -717,8 +718,8 @@ export default function ScriptReviewPage() {
gap: 2,
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<CodeIcon sx={{ fontSize: 20, color: '#007acc' }} />
<Typography sx={{ fontSize: '13px', fontWeight: 600, color: '#cccccc', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
<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' }}>
Script Review
</Typography>
</Box>
@@ -732,7 +733,7 @@ export default function ScriptReviewPage() {
>
Previous
</IDEButton>
<Typography sx={{ fontSize: '12px', color: '#858585', px: 1 }}>
<Typography sx={{ fontSize: '12px', color: text.muted, px: 1 }}>
{currentIndex + 1} / {allScripts.length}
</Typography>
<IDEButton
@@ -752,12 +753,12 @@ export default function ScriptReviewPage() {
gap: 1,
px: 1.5,
py: 0.5,
bgcolor: 'rgba(250, 200, 90, 0.15)',
bgcolor: `rgba(251, 191, 36, 0.15)`,
borderRadius: '2px',
border: '1px solid #fac85a',
border: `1px solid ${semantic.warning}`,
}}>
<WarningAmberIcon sx={{ fontSize: '14px', color: '#fac85a' }} />
<Typography sx={{ fontSize: '11px', color: '#fac85a', fontWeight: 500 }}>
<WarningAmberIcon sx={{ fontSize: '14px', color: semantic.warning }} />
<Typography sx={{ fontSize: '11px', color: semantic.warning, fontWeight: 500 }}>
UNSAVED CHANGES
</Typography>
</Box>
@@ -787,16 +788,16 @@ export default function ScriptReviewPage() {
width: 300,
minWidth: 300,
flexShrink: 0,
bgcolor: '#252526',
borderRight: '1px solid #2b2b2c',
bgcolor: surface.raised,
borderRight: `1px solid ${border.default}`,
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
}}>
{/* Script Info Section */}
<Box sx={{ p: 2, borderBottom: '1px solid #2b2b2c' }}>
<Box sx={{ p: 2, borderBottom: `1px solid ${border.default}` }}>
<Typography sx={{
color: '#858585',
color: text.muted,
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.5px',
@@ -806,11 +807,11 @@ export default function ScriptReviewPage() {
SCRIPT PROPERTIES
</Typography>
<Box sx={{ mb: 1.5 }}>
<Typography sx={{ color: '#858585', fontSize: '10px', mb: 0.5 }}>
<Typography sx={{ color: text.muted, fontSize: '10px', mb: 0.5 }}>
Name
</Typography>
<Typography sx={{
color: '#d4d4d4',
color: text.secondary,
fontFamily: '"Cascadia Code", "Courier New", monospace',
fontSize: '12px',
wordBreak: 'break-word',
@@ -819,11 +820,11 @@ export default function ScriptReviewPage() {
</Typography>
</Box>
<Box>
<Typography sx={{ color: '#858585', fontSize: '10px', mb: 0.5 }}>
<Typography sx={{ color: text.muted, fontSize: '10px', mb: 0.5 }}>
Hash
</Typography>
<Typography sx={{
color: '#858585',
color: text.muted,
fontFamily: '"Cascadia Code", "Courier New", monospace',
fontSize: '10px',
wordBreak: 'break-all',
@@ -834,9 +835,9 @@ export default function ScriptReviewPage() {
</Box>
{/* Policy Selection Section */}
<Box sx={{ p: 2, borderBottom: '1px solid #2b2b2c' }}>
<Box sx={{ p: 2, borderBottom: `1px solid ${border.default}` }}>
<Typography sx={{
color: '#858585',
color: text.muted,
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.5px',
@@ -929,9 +930,9 @@ export default function ScriptReviewPage() {
title="Purge Script & Policy"
style={{
padding: '6px',
backgroundColor: submitting || sourceChanged ? '#3e3e42' : '#7e0e0e',
backgroundColor: submitting || sourceChanged ? text.dim : '#7f1d1d',
color: '#ffffff',
border: `1px solid ${submitting || sourceChanged ? '#3e3e42' : '#b81414'}`,
border: `1px solid ${submitting || sourceChanged ? text.dim : semantic.error}`,
borderRadius: '2px',
cursor: submitting || sourceChanged ? 'not-allowed' : 'pointer',
opacity: submitting || sourceChanged ? 0.4 : 1,
@@ -946,22 +947,22 @@ export default function ScriptReviewPage() {
}}
onMouseEnter={(e) => {
if (!submitting && !sourceChanged) {
e.currentTarget.style.backgroundColor = '#9d0f0f';
e.currentTarget.style.backgroundColor = '#991b1b';
}
}}
onMouseLeave={(e) => {
if (!submitting && !sourceChanged) {
e.currentTarget.style.backgroundColor = '#7e0e0e';
e.currentTarget.style.backgroundColor = '#7f1d1d';
}
}}
onMouseDown={(e) => {
if (!submitting && !sourceChanged) {
e.currentTarget.style.backgroundColor = '#b81414';
e.currentTarget.style.backgroundColor = semantic.error;
}
}}
onMouseUp={(e) => {
if (!submitting && !sourceChanged) {
e.currentTarget.style.backgroundColor = '#9d0f0f';
e.currentTarget.style.backgroundColor = '#991b1b';
}
}}
>
@@ -976,15 +977,15 @@ export default function ScriptReviewPage() {
<Box sx={{ mt: 1 }}>
<Box sx={{
p: 1.5,
bgcolor: 'rgba(240, 82, 82, 0.15)',
border: '1px solid #f05252',
bgcolor: `rgba(248, 113, 113, 0.15)`,
border: `1px solid ${semantic.error}`,
borderRadius: '2px',
mb: 1,
}}>
<Typography sx={{ fontSize: '11px', color: '#f05252', fontWeight: 500, mb: 0.5 }}>
<Typography sx={{ fontSize: '11px', color: semantic.error, fontWeight: 500, mb: 0.5 }}>
Permanent Deletion
</Typography>
<Typography sx={{ fontSize: '10px', color: '#cccccc', lineHeight: 1.4 }}>
<Typography sx={{ fontSize: '10px', color: text.secondary, lineHeight: 1.4 }}>
This will permanently delete the script and policy. This action cannot be undone.
</Typography>
</Box>
@@ -1021,21 +1022,21 @@ export default function ScriptReviewPage() {
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{/* Tab Bar */}
<Box sx={{
bgcolor: '#2d2d2d',
borderBottom: '1px solid #1e1e1e',
bgcolor: '#27272a',
borderBottom: `1px solid ${surface.raised}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: 35,
}}>
<Box sx={{
bgcolor: '#1e1e1e',
bgcolor: surface.raised,
px: 2,
py: 0.75,
display: 'flex',
alignItems: 'center',
gap: 1,
borderTop: sourceChanged ? '2px solid #f59e0b' : '2px solid #007acc',
borderTop: sourceChanged ? `2px solid ${semantic.warning}` : `2px solid ${primary.dark}`,
color: '#ffffff',
minHeight: 35,
}}>
@@ -1048,7 +1049,7 @@ export default function ScriptReviewPage() {
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: '#f59e0b'
bgcolor: semantic.warning
}} />
)}
</Box>
@@ -1113,7 +1114,7 @@ export default function ScriptReviewPage() {
{/* Status Bar */}
<Box sx={{
bgcolor: '#007acc',
bgcolor: primary.dark,
color: 'white',
px: 2,
py: 0.5,
@@ -1152,9 +1153,9 @@ export default function ScriptReviewPage() {
onClose={handleCloseSnackbar}
severity={snackbar.severity}
sx={{
bgcolor: '#252526',
color: '#cccccc',
border: '1px solid #3e3e42',
bgcolor: surface.raised,
color: text.secondary,
border: `1px solid ${text.faint}`,
}}
>
{snackbar.message}

View File

@@ -6,6 +6,7 @@ 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,
@@ -407,7 +408,7 @@ export default function UserDashboardPage() {
gap: 2,
mb: 4
}}>
<Card sx={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<Card sx={{ background: `linear-gradient(135deg, ${primary.main} 0%, ${primary.dark} 100%)` }}>
<CardContent>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
Total Contributions
@@ -422,7 +423,7 @@ export default function UserDashboardPage() {
</CardContent>
</Card>
<Card sx={{ background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }}>
<Card sx={{ background: `linear-gradient(135deg, ${semantic.success} 0%, ${primary.mid} 100%)` }}>
<CardContent>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
Released
@@ -437,7 +438,7 @@ export default function UserDashboardPage() {
</CardContent>
</Card>
<Card sx={{ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }}>
<Card sx={{ background: `linear-gradient(135deg, ${semantic.info} 0%, ${primary.light} 100%)` }}>
<CardContent>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
In Review
@@ -452,7 +453,7 @@ export default function UserDashboardPage() {
</CardContent>
</Card>
<Card sx={{ background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }}>
<Card sx={{ background: `linear-gradient(135deg, ${semantic.error} 0%, ${semantic.warning} 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: "#2196f3" // blue
color: "#a78bfa" // purple
};
case 2:
return {
name: "Surf",
color: "#4caf50" // green
color: "#22d3ee" // cyan
};
case 5:
return {
name: "Fly Trials",
color: "#ff9800" // orange
color: "#fbbf24" // yellow
};
default:
return {