1 Commits
master ... int

Author SHA1 Message Date
49bee944d8 openapi: rethink integers
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-25 19:15:20 -07:00
181 changed files with 5453 additions and 32455 deletions

View File

@@ -7,54 +7,7 @@ platform:
arch: amd64
steps:
- name: build-backend
image: golang:1.24.0
environment:
GIT_USER:
from_secret: GIT_USER
GIT_PASS:
from_secret: GIT_PASS
commands:
- echo "machine git.itzana.me login $${GIT_USER} password $${GIT_PASS}" > ~/.netrc
- chmod 600 ~/.netrc
- make build-backend
when:
branch:
- master
- staging
- name: build-validator
image: rust:1.92
commands:
- make build-validator
when:
branch:
- master
- staging
- name: build-combobulator
image: rust:1.92
commands:
- make build-combobulator
when:
branch:
- master
- staging
- name: build-frontend
image: oven/bun:1.3.3
commands:
- apt-get update
- apt-get install make
- make build-frontend
when:
branch:
- master
- staging
event:
- pull_request
- name: image-backend
- name: api
image: plugins/docker
settings:
registry: registry.itzana.me
@@ -66,10 +19,8 @@ steps:
from_secret: REGISTRY_USER
password:
from_secret: REGISTRY_PASS
dockerfile: Dockerfile
dockerfile: Containerfile
context: .
depends_on:
- build-backend
when:
branch:
- master
@@ -77,7 +28,7 @@ steps:
event:
- push
- name: image-frontend
- name: frontend
image: plugins/docker
settings:
registry: registry.itzana.me
@@ -98,7 +49,7 @@ steps:
event:
- push
- name: image-validator
- name: validator
image: plugins/docker
settings:
registry: registry.itzana.me
@@ -111,32 +62,7 @@ steps:
password:
from_secret: REGISTRY_PASS
dockerfile: validation/Containerfile
context: .
depends_on:
- build-validator
when:
branch:
- master
- staging
event:
- push
- name: image-combobulator
image: plugins/docker
settings:
registry: registry.itzana.me
repo: registry.itzana.me/strafesnet/maptest-combobulator
tags:
- ${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
- ${DRONE_BRANCH}
username:
from_secret: REGISTRY_USER
password:
from_secret: REGISTRY_PASS
dockerfile: combobulator/Containerfile
context: .
depends_on:
- build-combobulator
context: validation
when:
branch:
- master
@@ -151,17 +77,15 @@ steps:
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-api:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-frontend:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-validator:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
- argocd app --grpc-web set ${DRONE_BRANCH}-maps-service --kustomize-image registry.itzana.me/strafesnet/maptest-combobulator:${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
environment:
USERNAME:
from_secret: ARGO_USER
PASSWORD:
from_secret: ARGO_PASS
depends_on:
- image-backend
- image-frontend
- image-validator
- image-combobulator
- api
- frontend
- validator
when:
branch:
- master
@@ -170,20 +94,64 @@ steps:
- push
# pr dry run
- name: api-pr
image: plugins/docker
settings:
registry: registry.itzana.me
repo: registry.itzana.me/strafesnet/maptest-validator
tags:
- ${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
- ${DRONE_BRANCH}
dockerfile: Containerfile
context: .
dry_run: true
when:
event:
- pull_request
- name: frontend-pr
image: plugins/docker
settings:
registry: registry.itzana.me
repo: registry.itzana.me/strafesnet/maptest-validator
tags:
- ${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
- ${DRONE_BRANCH}
dockerfile: web/Containerfile
context: web
dry_run: true
when:
event:
- pull_request
- name: validator-pr
image: plugins/docker
settings:
registry: registry.itzana.me
repo: registry.itzana.me/strafesnet/maptest-validator
tags:
- ${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
- ${DRONE_BRANCH}
dockerfile: validation/Containerfile
context: validation
dry_run: true
when:
event:
- pull_request
- name: build-pr
image: alpine
commands:
- echo "Success!"
depends_on:
- build-backend
- build-validator
- build-combobulator
- build-frontend
- api-pr
- frontend-pr
- validator-pr
when:
event:
- pull_request
---
kind: signature
hmac: a654fea05ccf642bb3a41ce777808ff995c8bd7286f2403fae179ce0db025619
hmac: 11e6d7f1eb839d3798fdcb642ca5523c011bd14c1f3a0343a9c3106bab9ef142
...

1
.gitignore vendored
View File

@@ -1,3 +1,2 @@
build
.idea
/target

3572
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,6 @@
[workspace]
members = [
"combobulator",
"validation",
"submissions-api-rs",
"validation/api",
]
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"] }

33
Containerfile Normal file
View File

@@ -0,0 +1,33 @@
# Stage 1: Build
FROM registry.itzana.me/docker-proxy/golang:1.24 AS builder
# Set the working directory in the container
WORKDIR /app
# Copy go.mod and go.sum files
COPY go.mod go.sum ./
# Copy the entire project
COPY . .
# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -o service ./cmd/maps-service/service.go
# Stage 2: Run
FROM registry.itzana.me/docker-proxy/alpine:3.21
# Set up a non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Set the working directory in the container
WORKDIR /home/appuser
# Copy the built application from the builder stage
COPY --from=builder /app/service .
# Expose application port (adjust if needed)
EXPOSE 8081
# Command to run the application
ENTRYPOINT ["./service"]

View File

@@ -1,3 +0,0 @@
FROM alpine
COPY build/server /
ENTRYPOINT ["/server"]

View File

@@ -1,50 +1,12 @@
clean:
rm -rf build
rm -rf web/build
submissions:
DOCKER_BUILDKIT=1 docker build . -f Containerfile -t maps-service-submissions
# build
build-backend:
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o build/server cmd/maps-service/service.go
web:
docker build web -f web/Containerfile -t maps-service-web
build-validator:
cargo build --release --bin maps-validation
validation:
docker build validation -f validation/Containerfile -t maps-service-validation
build-combobulator:
cargo build --release --bin maps-combobulator
all: submissions web validation
build-frontend:
rm -rf web/build
cd web && bun install --frozen-lockfile
cd web && bun run build
build: build-backend build-validator build-combobulator build-frontend
# image
image-backend:
docker build . -t maptest-api
image-validator:
docker build . -f validation/Containerfile -t maptest-validator
image-combobulator:
docker build . -f combobulator/Containerfile -t maptest-combobulator
image-frontend:
docker build web -f web/Containerfile -t maptest-frontend
# docker
docker-backend:
make build-backend
make image-backend
docker-validator:
make build-validator
make image-validator
docker-combobulator:
make build-combobulator
make image-combobulator
docker-frontend:
make image-frontend
docker: docker-backend docker-validator docker-combobulator docker-frontend
.PHONY: clean build-backend build-validator build-combobulator build-frontend build image-backend image-validator image-combobulator image-frontend docker-backend docker-validator docker-combobulator docker-frontend docker
.PHONY: submissions web validation

View File

@@ -13,11 +13,11 @@ Prerequisite: golang installed
1. Run `go generate` to ensure the generated API is up-to-date. This project uses [ogen](https://github.com/ogen-go/ogen).
```bash
go generate
go generate -run "go run github.com/ogen-go/ogen/cmd/ogen@latest --target api --clean openapi.yaml"
```
2. Build the project.
```bash
make build-backend
go build git.itzana.me/strafesnet/maps-service
```
By default, the project opens at `localhost:8080`.
@@ -47,16 +47,14 @@ AUTH_HOST="http://localhost:8083/"
Prerequisite: rust installed
1. `cargo run --release -p maps-validation`
1. `cd validation`
2. `cargo run --release`
Environment Variables:
- ROBLOX_GROUP_ID
- RBXCOOKIE
- RBX_API_KEY
- API_HOST_INTERNAL
- NATS_HOST
- LOAD_ASSET_VERSION_PLACE_ID
- LOAD_ASSET_VERSION_UNIVERSE_ID
#### License

View File

@@ -7,16 +7,6 @@ import (
"os"
)
// @title StrafesNET Maps API
// @version 1.0
// @description Obtain an api key at https://dev.strafes.net
// @description Requires Maps:Read permission
// @BasePath /public-api/v1
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name X-API-Key
func main() {
log.SetLevel(log.InfoLevel)
log.SetFormatter(&log.JSONFormatter{})
@@ -25,7 +15,6 @@ func main() {
app := cmds.NewApp()
app.Commands = []*cli.Command{
cmds.NewServeCommand(),
cmds.NewApiCommand(),
}
if err := app.Run(os.Args); err != nil {

View File

@@ -1,22 +0,0 @@
[package]
name = "maps-combobulator"
version = "0.1.0"
edition = "2024"
[dependencies]
async-nats.workspace = true
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1"
futures-util.workspace = true
map-tool = { version = "3.0.0", registry = "strafesnet", features = ["roblox"], default-features = false }
rbx_asset.workspace = true
rbx_binary.workspace = true
rbx_dom_weak.workspace = true
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
serde.workspace = true
serde_json.workspace = true
strafesnet_deferred_loader = { version = "0.6.0", registry = "strafesnet" }
strafesnet_rbx_loader = { version = "0.10.0", registry = "strafesnet" }
strafesnet_snf = { version = "0.3.2", registry = "strafesnet" }
tokio.workspace = true
tokio-stream = "0.1"

View File

@@ -1,4 +0,0 @@
FROM debian:trixie-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends libssl3t64 ca-certificates && rm -rf /var/lib/apt/lists/*
COPY /target/release/maps-combobulator /
ENTRYPOINT ["/maps-combobulator"]

View File

@@ -1,152 +0,0 @@
use std::collections::HashMap;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use strafesnet_rbx_loader::mesh::{MeshIndex,MeshType,MeshWithSize};
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum TextureError{
NoTexture,
RobloxAssetIdParse(RobloxAssetIdParseErr),
}
impl std::fmt::Display for TextureError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for TextureError{}
impl From<RobloxAssetIdParseErr> for TextureError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
pub struct TextureLoader{
textures:HashMap<RobloxAssetId,Texture>,
}
impl TextureLoader{
pub fn new()->Self{
Self{
textures:HashMap::new(),
}
}
pub fn insert(&mut self,asset_id:RobloxAssetId,texture:Vec<u8>){
self.textures.insert(asset_id,Texture::ImageDDS(texture));
}
}
impl Loader for TextureLoader{
type Error=TextureError;
type Index<'a>=&'a str;
type Resource=Texture;
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let asset_id:RobloxAssetId=index.parse()?;
let data=self.textures.get(&asset_id).ok_or(TextureError::NoTexture)?.clone();
Ok(data)
}
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum MeshError{
NoMesh,
RobloxAssetIdParse(RobloxAssetIdParseErr),
Mesh(strafesnet_rbx_loader::mesh::Error),
Union(strafesnet_rbx_loader::union::Error),
DecodeBinary(rbx_binary::DecodeError),
OneChildPolicy,
MissingInstance,
}
impl std::fmt::Display for MeshError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for MeshError{}
impl From<RobloxAssetIdParseErr> for MeshError{
fn from(value:RobloxAssetIdParseErr)->Self{
Self::RobloxAssetIdParse(value)
}
}
impl From<strafesnet_rbx_loader::mesh::Error> for MeshError{
fn from(value:strafesnet_rbx_loader::mesh::Error)->Self{
Self::Mesh(value)
}
}
impl From<strafesnet_rbx_loader::union::Error> for MeshError{
fn from(value:strafesnet_rbx_loader::union::Error)->Self{
Self::Union(value)
}
}
impl From<rbx_binary::DecodeError> for MeshError{
fn from(value:rbx_binary::DecodeError)->Self{
Self::DecodeBinary(value)
}
}
pub struct MeshLoader{
meshes:HashMap<RobloxAssetId,MeshWithSize>,
unions:HashMap<RobloxAssetId,rbx_dom_weak::WeakDom>,
}
impl MeshLoader{
pub fn new()->Self{
Self{
meshes:HashMap::new(),
unions:HashMap::new(),
}
}
pub fn insert_mesh(&mut self,asset_id:RobloxAssetId,mesh:MeshWithSize){
self.meshes.insert(asset_id,mesh);
}
pub fn insert_union(&mut self,asset_id:RobloxAssetId,union:rbx_dom_weak::WeakDom){
self.unions.insert(asset_id,union);
}
}
impl Loader for MeshLoader{
type Error=MeshError;
type Index<'a>=MeshIndex<'a>;
type Resource=MeshWithSize;
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
let id:RobloxAssetId=index.content.parse()?;
let mesh_with_size=self.meshes.get(&id).ok_or(MeshError::NoMesh)?;
mesh_with_size.clone()
},
MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{
// decode asset
let size=size_float_bits.map(f32::from_bits).into();
if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){
let id:RobloxAssetId=index.content.parse()?;
let dom=self.unions.get(&id).ok_or(MeshError::NoMesh)?;
let &[referent]=dom.root().children()else{
return Err(MeshError::OneChildPolicy);
};
let Some(instance)=dom.get_by_ref(referent)else{
return Err(MeshError::MissingInstance);
};
if physics_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("PhysicsData")){
physics_data=data.as_ref();
}
}
if mesh_data.is_empty(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&static_ustr("MeshData")){
mesh_data=data.as_ref();
}
}
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
}else{
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
}
},
};
Ok(mesh)
}
}

View File

@@ -1,165 +0,0 @@
use tokio_stream::StreamExt;
mod loader;
mod nats_types;
mod process;
mod s3;
const SUBJECT_MAPFIX_RELEASE:&str="maptest.mapfixes.release";
const SUBJECT_SUBMISSION_BATCHRELEASE:&str="maptest.submissions.batchrelease";
const SUBJECT_SEED:&str="maptest.combobulator.seed";
#[derive(Debug)]
pub enum StartupError{
NatsConnect(async_nats::ConnectError),
NatsGetStream(async_nats::jetstream::context::GetStreamError),
NatsConsumer(async_nats::jetstream::stream::ConsumerError),
NatsConsumerUpdate(async_nats::jetstream::stream::ConsumerUpdateError),
NatsStream(async_nats::jetstream::consumer::StreamError),
}
impl std::fmt::Display for StartupError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for StartupError{}
#[expect(dead_code)]
#[derive(Debug)]
enum HandleMessageError{
Json(serde_json::Error),
UnknownSubject(String),
Process(process::Error),
Ack(async_nats::Error),
Publish(async_nats::jetstream::context::PublishError),
}
impl std::fmt::Display for HandleMessageError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for HandleMessageError{}
fn from_slice<'a,T:serde::de::Deserialize<'a>>(slice:&'a [u8])->Result<T,HandleMessageError>{
serde_json::from_slice(slice).map_err(HandleMessageError::Json)
}
async fn handle_message(
processor:&process::Processor,
jetstream:&async_nats::jetstream::Context,
message:async_nats::jetstream::Message,
)->Result<(),HandleMessageError>{
match message.subject.as_str(){
SUBJECT_MAPFIX_RELEASE=>{
let request:nats_types::ReleaseMapfixRequest=from_slice(&message.payload)?;
processor.handle_mapfix_release(request).await.map_err(HandleMessageError::Process)?;
message.ack().await.map_err(HandleMessageError::Ack)?;
},
SUBJECT_SUBMISSION_BATCHRELEASE=>{
// split batch into individual seed messages
let batch:nats_types::ReleaseSubmissionsBatchRequest=from_slice(&message.payload)?;
println!("[combobulator] Splitting batch release (operation {}, {} submissions)",
batch.OperationID,batch.Submissions.len());
for submission in batch.Submissions{
let seed=nats_types::SeedCombobulatorRequest{AssetID:submission.UploadedAssetID};
let payload=serde_json::to_vec(&seed).map_err(HandleMessageError::Json)?;
jetstream.publish(SUBJECT_SEED,payload.into())
.await.map_err(HandleMessageError::Publish)?;
println!("[combobulator] Queued seed for asset {}",seed.AssetID);
}
message.ack().await.map_err(HandleMessageError::Ack)?;
},
SUBJECT_SEED=>{
let request:nats_types::SeedCombobulatorRequest=from_slice(&message.payload)?;
processor.handle_seed(request).await.map_err(HandleMessageError::Process)?;
message.ack().await.map_err(HandleMessageError::Ack)?;
},
other=>return Err(HandleMessageError::UnknownSubject(other.to_owned())),
}
println!("[combobulator] Message processed and acked");
Ok(())
}
#[tokio::main]
async fn main()->Result<(),StartupError>{
// roblox cookie api for downloading assets
let cookie=std::env::var("RBXCOOKIE").expect("RBXCOOKIE env required");
let cookie_context=rbx_asset::cookie::Context::new(rbx_asset::cookie::Cookie::new(cookie));
// s3
let s3_bucket=std::env::var("S3_BUCKET").expect("S3_BUCKET env required");
let s3_config=aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let s3_client=aws_sdk_s3::Client::new(&s3_config);
let s3_cache=s3::S3Cache::new(s3_client,s3_bucket);
let processor=process::Processor{
cookie_context,
s3:s3_cache,
};
// nats
let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required");
const STREAM_NAME:&str="maptest";
const DURABLE_NAME:&str="combobulator";
let filter_subjects=vec![
SUBJECT_MAPFIX_RELEASE.to_owned(),
SUBJECT_SUBMISSION_BATCHRELEASE.to_owned(),
SUBJECT_SEED.to_owned(),
];
let nats_config=async_nats::jetstream::consumer::pull::Config{
name:Some(DURABLE_NAME.to_owned()),
durable_name:Some(DURABLE_NAME.to_owned()),
filter_subjects:filter_subjects.clone(),
ack_wait:std::time::Duration::from_secs(900), // 15 minutes for processing
max_deliver:5, // retry up to 5 times
..Default::default()
};
let nasty=async_nats::connect(nats_host).await.map_err(StartupError::NatsConnect)?;
let jetstream=async_nats::jetstream::new(nasty);
let stream=jetstream.get_stream(STREAM_NAME).await.map_err(StartupError::NatsGetStream)?;
let consumer=stream.get_or_create_consumer(DURABLE_NAME,nats_config.clone()).await.map_err(StartupError::NatsConsumer)?;
// update consumer config if filter subjects changed
if consumer.cached_info().config.filter_subjects!=filter_subjects{
stream.update_consumer(nats_config).await.map_err(StartupError::NatsConsumerUpdate)?;
}
let mut messages=consumer.messages().await.map_err(StartupError::NatsStream)?;
// SIGTERM graceful shutdown
let mut sig_term=tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("Failed to create SIGTERM signal listener");
println!("[combobulator] Started, waiting for messages...");
// sequential processing loop - one message at a time
let main_loop=async{
while let Some(message_result)=messages.next().await{
match message_result{
Ok(message)=>{
match handle_message(&processor,&jetstream,message).await{
Ok(())=>println!("[combobulator] Success"),
Err(e)=>println!("[combobulator] Error: {e}"),
}
},
Err(e)=>println!("[combobulator] Message stream error: {e}"),
}
}
};
tokio::select!{
_=sig_term.recv()=>{
println!("[combobulator] Received SIGTERM, shutting down");
},
_=main_loop=>{
println!("[combobulator] Message stream ended");
},
};
Ok(())
}

View File

@@ -1,35 +0,0 @@
#[expect(nonstandard_style,dead_code)]
#[derive(serde::Deserialize)]
pub struct ReleaseMapfixRequest{
pub MapfixID:u64,
pub ModelID:u64,
pub ModelVersion:u64,
pub TargetAssetID:u64,
}
#[expect(nonstandard_style,dead_code)]
#[derive(serde::Deserialize)]
pub struct ReleaseSubmissionRequest{
pub SubmissionID:u64,
pub ReleaseDate:i64,
pub ModelID:u64,
pub ModelVersion:u64,
pub UploadedAssetID:u64,
pub DisplayName:String,
pub Creator:String,
pub GameID:u32,
pub Submitter:u64,
}
#[expect(nonstandard_style)]
#[derive(serde::Deserialize)]
pub struct ReleaseSubmissionsBatchRequest{
pub Submissions:Vec<ReleaseSubmissionRequest>,
pub OperationID:u32,
}
#[expect(nonstandard_style)]
#[derive(serde::Deserialize,serde::Serialize)]
pub struct SeedCombobulatorRequest{
pub AssetID:u64,
}

View File

@@ -1,280 +0,0 @@
use std::io::Cursor;
use crate::nats_types::ReleaseMapfixRequest;
use crate::s3::S3Cache;
use futures_util::stream::iter as stream_iter;
use futures_util::{StreamExt,TryStreamExt};
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
const CONCURRENT_REQUESTS:usize=16;
#[expect(dead_code)]
#[derive(Debug)]
pub enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
RobloxLoadMesh(super::loader::MeshError),
RobloxLoadTexture(super::loader::TextureError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
pub fn convert_to_snf(
dom:rbx_dom_weak::WeakDom,
mut mesh_loader:crate::loader::MeshLoader,
mut texture_loader:crate::loader::TextureLoader,
)->Result<Vec<u8>,ConvertError>{
const FAILURE_MODE:LoadFailureMode=LoadFailureMode::DefaultToNone;
// run scripts
let model=strafesnet_rbx_loader::Model::new(dom);
let mut place=strafesnet_rbx_loader::Place::from(model);
// TODO: script errors report for burn down chart
let _script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
// convert
let mut texture_deferred_loader=strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=strafesnet_deferred_loader::deferred_loader::MeshDeferredLoader::new();
let map_step1=strafesnet_rbx_loader::rbx::convert(
place.as_ref(),
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadMesh)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadTexture)?;
// TODO: conversion error report for burn down chart
let (map,_convert_errors)=map_step2.add_render_configs_and_textures(render_configs);
let mut snf_buf=Vec::new();
strafesnet_snf::map::write_map(Cursor::new(&mut snf_buf),map).map_err(ConvertError::SNFMap)?;
Ok(snf_buf)
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum Error{
ArchivedModel,
LoadDom(map_tool::roblox::LoadDomError),
DownloadAsset(map_tool::roblox::DownloadAssetError),
ConvertSnf(ConvertError),
S3Get(crate::s3::GetError),
S3Put(crate::s3::PutError),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for Error{}
pub struct Processor{
pub cookie_context:rbx_asset::cookie::Context,
pub s3:S3Cache,
}
impl Processor{
/// Download an asset, returning None if the asset is archived.
async fn download_asset(&self,asset_id:u64)->Result<Option<Vec<u8>>,Error>{
match map_tool::roblox::download_asset(&self.cookie_context,asset_id).await{
Ok(data)=>Ok(Some(data)),
Err(e)=>{
let s=format!("{e:?}");
if s.contains("Requested asset is archived"){
println!("[combobulator] Asset {asset_id} is archived, skipping");
Ok(None)
}else if s.contains("User is not authorized to access Asset"){
println!("[combobulator] User is not authorized to access Asset {asset_id}, skipping");
Ok(None)
}else if s.contains("Asset is not approved for the requester"){
println!("[combobulator] Asset {asset_id} is not approved for the requester, skipping");
Ok(None)
}else if s.contains("Request asset was not found"){
println!("[combobulator] Asset {asset_id} was not found, skipping");
Ok(None)
}else{
Err(Error::DownloadAsset(e))
}
}
}
}
/// Process a single model: extract assets, cache to S3, build SNF.
async fn process_model(&self,asset_id:u64)->Result<(),Error>{
println!("[combobulator] Downloading model {asset_id}");
let rbxl_bytes=self.download_asset(asset_id).await?
.ok_or(Error::ArchivedModel)?;
// decode dom
let dom=map_tool::roblox::load_dom(&rbxl_bytes)
.map_err(Error::LoadDom)?;
// extract unique assets from the file
let assets=map_tool::roblox::get_unique_assets(&dom);
// place textures into 'loader'
let texture_loader=crate::loader::TextureLoader::new();
// process textures: download, cache, convert to DDS
let texture_loader=stream_iter(assets.textures).map(async|id|{
let asset_id=id.0;
let dds_key=S3Cache::texture_dds_key(asset_id);
// fetch cached DDS
let dds=if let Some(dds)=self.s3.get(&dds_key).await.map_err(Error::S3Get)?{
dds
}else{
// check raw cache, download if missing
let raw_key=S3Cache::texture_raw_key(asset_id);
let dds_result=if let Some(data)=self.s3.get(&raw_key).await.map_err(Error::S3Get)?{
map_tool::roblox::convert_texture_to_dds(&data)
}else{
println!("[combobulator] Downloading texture {asset_id}");
let Some(data)=self.download_asset(asset_id).await? else{
return Ok(None);
};
// decode while we have ownership
let dds_result=map_tool::roblox::convert_texture_to_dds(&data);
self.s3.put(&raw_key,data).await.map_err(Error::S3Put)?;
dds_result
};
// handle error after cacheing data
let dds=match dds_result{
Ok(dds)=>dds,
Err(e)=>{
println!("[combobulator] Texture {asset_id} convert error: {e}");
return Ok(None);
}
};
self.s3.put(&dds_key,dds.clone()).await.map_err(Error::S3Put)?;
dds
};
println!("[combobulator] Texture {asset_id} processed");
Ok(Some((id,dds)))
})
.buffer_unordered(CONCURRENT_REQUESTS)
.try_fold(texture_loader,async|mut texture_loader,maybe_loaded_texture|{
if let Some((id,dds))=maybe_loaded_texture{
texture_loader.insert(id,dds);
}
Ok(texture_loader)
}).await?;
let mesh_loader=crate::loader::MeshLoader::new();
// process meshes
let mesh_loader=stream_iter(assets.meshes).map(async|id|{
let asset_id=id.0;
let mesh_key=S3Cache::mesh_key(asset_id);
let mesh_result=if let Some(data)=self.s3.get(&mesh_key).await.map_err(Error::S3Get)?{
strafesnet_rbx_loader::mesh::convert(&data)
}else{
println!("[combobulator] Downloading mesh {asset_id}");
let Some(data)=self.download_asset(asset_id).await? else{
return Ok(None);
};
// decode while we have ownership
let mesh_result=strafesnet_rbx_loader::mesh::convert(&data);
self.s3.put(&mesh_key,data.clone()).await.map_err(Error::S3Put)?;
mesh_result
};
println!("[combobulator] Mesh {asset_id} processed");
// handle error after cacheing data
match mesh_result{
Ok(mesh)=>Ok(Some((id,mesh))),
Err(e)=>{
println!("[combobulator] Mesh {asset_id} convert error: {e}");
Ok(None)
},
}
})
.buffer_unordered(CONCURRENT_REQUESTS)
.try_fold(mesh_loader,async|mut mesh_loader,maybe_loaded_mesh|{
if let Some((id,mesh))=maybe_loaded_mesh{
mesh_loader.insert_mesh(id,mesh);
}
Ok(mesh_loader)
}).await?;
// process unions
let mesh_loader=stream_iter(assets.unions).map(async|id|{
let asset_id=id.0;
let union_key=S3Cache::union_key(asset_id);
let union_result=if let Some(data)=self.s3.get(&union_key).await.map_err(Error::S3Get)?{
rbx_binary::from_reader(data.as_slice())
}else{
println!("[combobulator] Downloading union {asset_id}");
let Some(data)=self.download_asset(asset_id).await? else{
return Ok(None);
};
// decode the data while we have ownership
let union_result=rbx_binary::from_reader(data.as_slice());
self.s3.put(&union_key,data).await.map_err(Error::S3Put)?;
union_result
};
println!("[combobulator] Union {asset_id} processed");
// handle error after cacheing data
match union_result{
Ok(union)=>Ok(Some((id,union))),
Err(e)=>{
println!("[combobulator] Union {asset_id} convert error: {e}");
Ok(None)
},
}
})
.buffer_unordered(CONCURRENT_REQUESTS)
.try_fold(mesh_loader,async|mut mesh_loader,maybe_loaded_union|{
if let Some((id,union))=maybe_loaded_union{
mesh_loader.insert_union(id,union);
}
Ok(mesh_loader)
}).await?;
// convert to SNF and upload
println!("[combobulator] Converting to SNF");
let snf=convert_to_snf(dom,mesh_loader,texture_loader)
.map_err(Error::ConvertSnf)?;
let snf_key=S3Cache::snf_key(asset_id);
self.s3.put(&snf_key,snf).await.map_err(Error::S3Put)?;
println!("[combobulator] SNF uploaded to {snf_key}");
Ok(())
}
/// Handle a mapfix release message.
pub async fn handle_mapfix_release(&self,request:ReleaseMapfixRequest)->Result<(),Error>{
println!("[combobulator] Processing mapfix {} (asset {})",
request.MapfixID,request.TargetAssetID);
self.process_model(request.TargetAssetID).await
}
/// Handle a seed request (reprocess an existing map).
pub async fn handle_seed(&self,request:crate::nats_types::SeedCombobulatorRequest)->Result<(),Error>{
println!("[combobulator] Seeding asset {}",request.AssetID);
self.process_model(request.AssetID).await
}
}

View File

@@ -1,96 +0,0 @@
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
#[expect(dead_code)]
#[derive(Debug)]
pub enum GetError{
Get(aws_sdk_s3::error::SdkError<aws_sdk_s3::operation::get_object::GetObjectError>),
Collect(aws_sdk_s3::primitives::ByteStreamError),
}
impl std::fmt::Display for GetError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for GetError{}
#[expect(dead_code)]
#[derive(Debug)]
pub enum PutError{
Put(aws_sdk_s3::error::SdkError<aws_sdk_s3::operation::put_object::PutObjectError>),
}
impl std::fmt::Display for PutError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for PutError{}
pub struct S3Cache{
client:Client,
bucket:String,
}
impl S3Cache{
pub fn new(client:Client,bucket:String)->Self{
Self{client,bucket}
}
/// Try to get a cached object. Returns None if the key doesn't exist.
pub async fn get(&self,key:&str)->Result<Option<Vec<u8>>,GetError>{
match self.client.get_object()
.bucket(&self.bucket)
.key(key)
.send()
.await
{
Ok(output)=>{
let bytes=output.body.collect().await.map_err(GetError::Collect)?;
Ok(Some(bytes.to_vec()))
},
Err(e)=>{
// check if it's a NoSuchKey error
if let aws_sdk_s3::error::SdkError::ServiceError(ref service_err)=e{
if service_err.err().is_no_such_key(){
return Ok(None);
}
}
Err(GetError::Get(e))
},
}
}
/// Put an object into S3.
pub async fn put(&self,key:&str,data:Vec<u8>)->Result<(),PutError>{
self.client.put_object()
.bucket(&self.bucket)
.key(key)
.body(ByteStream::from(data))
.send()
.await
.map_err(PutError::Put)?;
Ok(())
}
// S3 key helpers
pub fn texture_raw_key(asset_id:u64)->String{
format!("assets/textures/{asset_id}.raw")
}
pub fn texture_dds_key(asset_id:u64)->String{
format!("assets/textures/{asset_id}.dds")
}
pub fn mesh_key(asset_id:u64)->String{
format!("assets/meshes/{asset_id}")
}
pub fn union_key(asset_id:u64)->String{
format!("assets/unions/{asset_id}")
}
pub fn snf_key(model_id:u64)->String{
format!("maps/{model_id}.snfm")
}
}

View File

@@ -13,7 +13,7 @@ services:
submissions:
image:
maptest-api
maps-service-submissions
container_name: submissions
command: [
# debug
@@ -34,7 +34,7 @@ services:
"--data-rpc-host","dataservice:9000",
]
env_file:
- /home/quat/auth-compose/strafesnet_staging.env
- ../auth-compose/strafesnet_staging.env
depends_on:
- authrpc
- nats
@@ -45,7 +45,7 @@ services:
web:
image:
maptest-frontend
maps-service-web
networks:
- maps-service-network
ports:
@@ -56,16 +56,14 @@ services:
validation:
image:
maptest-validator
maps-service-validation
container_name: validation
env_file:
- /home/quat/auth-compose/strafesnet_staging.env
- ../auth-compose/strafesnet_staging.env
environment:
- ROBLOX_GROUP_ID=17032139 # "None" is special case string value
- API_HOST_INTERNAL=http://submissions:8083/v1
- NATS_HOST=nats:4222
- LOAD_ASSET_VERSION_PLACE_ID=14001440964
- LOAD_ASSET_VERSION_UNIVERSE_ID=4850603885
depends_on:
- nats
# note: this races the submissions which creates a nats stream
@@ -105,7 +103,7 @@ services:
- REDIS_ADDR=authredis:6379
- RBX_GROUP_ID=17032139
env_file:
- /home/quat/auth-compose/auth-service.env
- ../auth-compose/auth-service.env
depends_on:
- authredis
networks:
@@ -119,7 +117,7 @@ services:
environment:
- REDIS_ADDR=authredis:6379
env_file:
- /home/quat/auth-compose/auth-service.env
- ../auth-compose/auth-service.env
depends_on:
- authredis
networks:

View File

@@ -1,282 +0,0 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/map": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get a list of maps",
"produces": [
"application/json"
],
"tags": [
"maps"
],
"summary": "List maps",
"parameters": [
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"default": 10,
"description": "Page size (max 100)",
"name": "page_size",
"in": "query"
},
{
"minimum": 1,
"type": "integer",
"default": 1,
"description": "Page number",
"name": "page_number",
"in": "query"
},
{
"type": "integer",
"name": "game_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/PagedResponse-Map"
}
},
"default": {
"description": "General error response",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/map/{id}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get a specific map by its ID",
"produces": [
"application/json"
],
"tags": [
"maps"
],
"summary": "Get map by ID",
"parameters": [
{
"type": "integer",
"description": "Map ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Response-Map"
}
},
"404": {
"description": "Map not found",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "General error response",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/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": {
"Error": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"Map": {
"type": "object",
"properties": {
"asset_version": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"creator": {
"type": "string"
},
"date": {
"type": "string"
},
"display_name": {
"type": "string"
},
"game_id": {
"type": "integer"
},
"id": {
"type": "integer"
},
"load_count": {
"type": "integer"
},
"modes": {
"type": "integer"
},
"submitter": {
"type": "integer"
},
"thumbnail": {
"type": "integer"
},
"updated_at": {
"type": "string"
}
}
},
"PagedResponse-Map": {
"type": "object",
"properties": {
"data": {
"description": "Data contains the actual response payload",
"type": "array",
"items": {
"$ref": "#/definitions/Map"
}
},
"pagination": {
"description": "Pagination contains information about paging",
"allOf": [
{
"$ref": "#/definitions/Pagination"
}
]
}
}
},
"Pagination": {
"type": "object",
"properties": {
"page": {
"description": "Current page number",
"type": "integer"
},
"page_size": {
"description": "Number of items per page",
"type": "integer"
}
}
},
"Response-Map": {
"type": "object",
"properties": {
"data": {
"description": "Data contains the actual response payload",
"allOf": [
{
"$ref": "#/definitions/Map"
}
]
}
}
}
},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "X-API-Key",
"in": "header"
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "",
BasePath: "/public-api/v1",
Schemes: []string{},
Title: "StrafesNET Maps API",
Description: "Obtain an api key at https://dev.strafes.net\nRequires Maps:Read permission",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

View File

@@ -1,257 +0,0 @@
{
"swagger": "2.0",
"info": {
"description": "Obtain an api key at https://dev.strafes.net\nRequires Maps:Read permission",
"title": "StrafesNET Maps API",
"contact": {},
"version": "1.0"
},
"basePath": "/public-api/v1",
"paths": {
"/map": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get a list of maps",
"produces": [
"application/json"
],
"tags": [
"maps"
],
"summary": "List maps",
"parameters": [
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"default": 10,
"description": "Page size (max 100)",
"name": "page_size",
"in": "query"
},
{
"minimum": 1,
"type": "integer",
"default": 1,
"description": "Page number",
"name": "page_number",
"in": "query"
},
{
"type": "integer",
"name": "game_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/PagedResponse-Map"
}
},
"default": {
"description": "General error response",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/map/{id}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get a specific map by its ID",
"produces": [
"application/json"
],
"tags": [
"maps"
],
"summary": "Get map by ID",
"parameters": [
{
"type": "integer",
"description": "Map ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Response-Map"
}
},
"404": {
"description": "Map not found",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "General error response",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/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": {
"Error": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"Map": {
"type": "object",
"properties": {
"asset_version": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"creator": {
"type": "string"
},
"date": {
"type": "string"
},
"display_name": {
"type": "string"
},
"game_id": {
"type": "integer"
},
"id": {
"type": "integer"
},
"load_count": {
"type": "integer"
},
"modes": {
"type": "integer"
},
"submitter": {
"type": "integer"
},
"thumbnail": {
"type": "integer"
},
"updated_at": {
"type": "string"
}
}
},
"PagedResponse-Map": {
"type": "object",
"properties": {
"data": {
"description": "Data contains the actual response payload",
"type": "array",
"items": {
"$ref": "#/definitions/Map"
}
},
"pagination": {
"description": "Pagination contains information about paging",
"allOf": [
{
"$ref": "#/definitions/Pagination"
}
]
}
}
},
"Pagination": {
"type": "object",
"properties": {
"page": {
"description": "Current page number",
"type": "integer"
},
"page_size": {
"description": "Number of items per page",
"type": "integer"
}
}
},
"Response-Map": {
"type": "object",
"properties": {
"data": {
"description": "Data contains the actual response payload",
"allOf": [
{
"$ref": "#/definitions/Map"
}
]
}
}
}
},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "X-API-Key",
"in": "header"
}
}
}

View File

@@ -1,166 +0,0 @@
basePath: /public-api/v1
definitions:
Error:
properties:
error:
type: string
type: object
Map:
properties:
asset_version:
type: integer
created_at:
type: string
creator:
type: string
date:
type: string
display_name:
type: string
game_id:
type: integer
id:
type: integer
load_count:
type: integer
modes:
type: integer
submitter:
type: integer
thumbnail:
type: integer
updated_at:
type: string
type: object
PagedResponse-Map:
properties:
data:
description: Data contains the actual response payload
items:
$ref: '#/definitions/Map'
type: array
pagination:
allOf:
- $ref: '#/definitions/Pagination'
description: Pagination contains information about paging
type: object
Pagination:
properties:
page:
description: Current page number
type: integer
page_size:
description: Number of items per page
type: integer
type: object
Response-Map:
properties:
data:
allOf:
- $ref: '#/definitions/Map'
description: Data contains the actual response payload
type: object
info:
contact: {}
description: |-
Obtain an api key at https://dev.strafes.net
Requires Maps:Read permission
title: StrafesNET Maps API
version: "1.0"
paths:
/map:
get:
description: Get a list of maps
parameters:
- default: 10
description: Page size (max 100)
in: query
maximum: 100
minimum: 1
name: page_size
type: integer
- default: 1
description: Page number
in: query
minimum: 1
name: page_number
type: integer
- in: query
name: game_id
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/PagedResponse-Map'
default:
description: General error response
schema:
$ref: '#/definitions/Error'
security:
- ApiKeyAuth: []
summary: List maps
tags:
- maps
/map/{id}:
get:
description: Get a specific map by its ID
parameters:
- description: Map ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/Response-Map'
"404":
description: Map not found
schema:
$ref: '#/definitions/Error'
default:
description: General error response
schema:
$ref: '#/definitions/Error'
security:
- ApiKeyAuth: []
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
name: X-API-Key
type: apiKey
swagger: "2.0"

View File

@@ -1,4 +1,3 @@
package main
//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

116
go.mod
View File

@@ -1,124 +1,64 @@
module git.itzana.me/strafesnet/maps-service
go 1.24.0
go 1.22
toolchain go1.24.5
toolchain go1.23.3
require (
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301211036-f2db3cb46e8c
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815
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
github.com/go-faster/errors v0.7.1
github.com/go-faster/jx v1.2.0
github.com/go-faster/jx v1.1.0
github.com/nats-io/nats.go v1.37.0
github.com/ogen-go/ogen v1.18.0
github.com/redis/go-redis/v9 v9.10.0
github.com/ogen-go/ogen v1.2.1
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.6
github.com/urfave/cli/v2 v2.27.6
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
github.com/urfave/cli/v2 v2.27.5
go.opentelemetry.io/otel v1.32.0
go.opentelemetry.io/otel/metric v1.32.0
go.opentelemetry.io/otel/trace v1.32.0
google.golang.org/grpc v1.48.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.25.12
gorm.io/driver/postgres v1.5.10
gorm.io/gorm v1.25.10
)
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
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/klauspost/compress v1.17.6 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)
require (
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-faster/yaml v0.4.6 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
// github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

304
go.sum
View File

@@ -1,78 +1,14 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed h1:eGWIQx2AOrSsLC2dieuSs8MCliRE60tvpZnmxsTBtKc=
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628052121-92af8193b5ed/go.mod h1:KJal0K++M6HEzSry6JJ2iDPZtOQn5zSstNlDbU3X4Jg=
git.itzana.me/strafesnet/go-grpc v0.0.0-20251228204118-c20dbb42afec h1:JSar9If1kzb02+Erp+zmSqHKWPPP2NqMQVK15pRmkLE=
git.itzana.me/strafesnet/go-grpc v0.0.0-20251228204118-c20dbb42afec/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301210537-0bea64387f6d h1:I73hWqmIcsSH90VHjwsg50v6emQkM0IAA04vb4wktBA=
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301210537-0bea64387f6d/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301211036-f2db3cb46e8c h1:sI50ymozoI+HFbxg1AOdCeWF6bJgpeP6OrnCvyjuQ9U=
git.itzana.me/strafesnet/go-grpc v0.0.0-20260301211036-f2db3cb46e8c/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815 h1:hkuOnehphRXUq/2z2UYgoqTq5MJj1GsWfshyc7bXda8=
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815/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=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
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=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
@@ -81,65 +17,34 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -163,9 +68,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -173,207 +77,122 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/ogen-go/ogen v1.18.0 h1:6RQ7lFBjOeNaUWu4getfqIh4GJbEY4hqKuzDtec/g60=
github.com/ogen-go/ogen v1.18.0/go.mod h1:dHFr2Wf6cA7tSxMI+zPC21UR5hAlDw8ZYUkK3PziURY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/ogen-go/ogen v1.2.1 h1:C5A0lvUMu2wl+eWIxnpXMWnuOJ26a2FyzR1CIC2qG0M=
github.com/ogen-go/ogen v1.2.1/go.mod h1:P2zQdEu8UqaVRfD5GEFvl+9q63VjMLvDquq1wVbyInM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel 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/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -403,11 +222,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -415,15 +232,12 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/postgres v1.5.10 h1:7Lggqempgy496c0WfHXsYWxk3Th+ZcW66/21QhVFdeE=
gorm.io/driver/postgres v1.5.10/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.21.11/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -14,41 +14,15 @@ tags:
description: Long-running operations
- name: Session
description: Session queries
- name: Stats
description: Statistics queries
- name: Submissions
description: Submission operations
- name: Scripts
description: Script operations
- name: ScriptPolicy
description: Script policy operations
- name: Thumbnails
description: Thumbnail operations
- name: Users
description: User operations
security:
- cookieAuth: []
paths:
/stats:
get:
summary: Get aggregate statistics
operationId: getStats
tags:
- Stats
security: []
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: "#/components/schemas/Stats"
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/session/user:
get:
summary: Get information about the currently logged in user
@@ -106,6 +80,21 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/migrate-maps:
post:
summary: Perform maps migration
operationId: migrateMaps
tags:
- Maps
responses:
"200":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/maps:
get:
summary: Get list of maps
@@ -130,14 +119,14 @@ paths:
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 1
maximum: 5
- name: Sort
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 0
maximum: 4
description: >
@@ -184,29 +173,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/maps/{MapID}/combobulate:
post:
summary: Queue a map for combobulator processing
operationId: combobulateMap
tags:
- Maps
parameters:
- name: MapID
in: path
required: true
schema:
type: integer
format: int64
minimum: 0
responses:
"204":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/maps/{MapID}/download:
get:
summary: Download the map asset
@@ -235,21 +201,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/maps-admin/seed-combobulator:
post:
summary: Queue all maps for combobulator processing
operationId: seedCombobulator
tags:
- Maps
responses:
"204":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mapfixes:
get:
summary: Get list of mapfixes
@@ -274,7 +225,7 @@ paths:
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 1
maximum: 5
description: >
@@ -286,7 +237,7 @@ paths:
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 0
maximum: 4
description: >
@@ -300,31 +251,25 @@ paths:
in: query
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: AssetID
in: query
schema:
type: integer
format: int64
minimum: 0
- name: AssetVersion
in: query
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: TargetAssetID
in: query
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: StatusID
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 0
maximum: 9
description: >
@@ -467,14 +412,14 @@ paths:
required: true
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: ModelVersion
in: query
required: true
schema:
type: integer
format: int64
format: uint64
minimum: 0
responses:
"204":
@@ -485,30 +430,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/description:
patch:
summary: Update description (submitter only)
operationId: updateMapfixDescription
tags:
- Mapfixes
parameters:
- $ref: '#/components/parameters/MapfixID'
requestBody:
required: true
content:
text/plain:
schema:
type: string
maxLength: 256
responses:
"204":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/completed:
post:
summary: Called by maptest when a player completes the map
@@ -681,7 +602,7 @@ paths:
$ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/status/trigger-upload:
post:
summary: Role MapfixUpload changes status from Validated -> Uploading
summary: Role Admin changes status from Validated -> Uploading
operationId: actionMapfixTriggerUpload
tags:
- Mapfixes
@@ -698,7 +619,7 @@ paths:
$ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/status/reset-uploading:
post:
summary: Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated
summary: Role Admin manually resets uploading softlock and changes status from Uploading -> Validated
operationId: actionMapfixValidated
tags:
- Mapfixes
@@ -713,40 +634,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/status/trigger-release:
post:
summary: Role MapfixUpload changes status from Uploaded -> Releasing
operationId: actionMapfixTriggerRelease
tags:
- Mapfixes
parameters:
- $ref: '#/components/parameters/MapfixID'
responses:
"204":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/mapfixes/{MapfixID}/status/reset-releasing:
post:
summary: Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded
operationId: actionMapfixUploaded
tags:
- Mapfixes
parameters:
- $ref: '#/components/parameters/MapfixID'
responses:
"204":
description: Successful response
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/operations/{OperationID}:
get:
summary: Retrieve operation with ID
@@ -792,7 +679,7 @@ paths:
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 1
maximum: 5
description: >
@@ -804,7 +691,7 @@ paths:
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 0
maximum: 4
description: >
@@ -818,31 +705,25 @@ paths:
in: query
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: AssetID
in: query
schema:
type: integer
format: int64
minimum: 0
- name: AssetVersion
in: query
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: UploadedAssetID
in: query
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: StatusID
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 0
maximum: 10
description: >
@@ -1011,14 +892,14 @@ paths:
required: true
schema:
type: integer
format: int64
format: uint64
minimum: 0
- name: ModelVersion
in: query
required: true
schema:
type: integer
format: int64
format: uint64
minimum: 0
responses:
"204":
@@ -1201,7 +1082,7 @@ paths:
$ref: "#/components/schemas/Error"
/submissions/{SubmissionID}/status/trigger-upload:
post:
summary: Role SubmissionUpload changes status from Validated -> Uploading
summary: Role Admin changes status from Validated -> Uploading
operationId: actionSubmissionTriggerUpload
tags:
- Submissions
@@ -1218,7 +1099,7 @@ paths:
$ref: "#/components/schemas/Error"
/submissions/{SubmissionID}/status/reset-uploading:
post:
summary: Role SubmissionUpload manually resets uploading softlock and changes status from Uploading -> Validated
summary: Role Admin manually resets uploading softlock and changes status from Uploading -> Validated
operationId: actionSubmissionValidated
tags:
- Submissions
@@ -1235,7 +1116,7 @@ paths:
$ref: "#/components/schemas/Error"
/release-submissions:
post:
summary: Release a set of uploaded maps. Role SubmissionRelease
summary: Release a set of uploaded maps
operationId: releaseSubmissions
tags:
- Submissions
@@ -1252,10 +1133,6 @@ paths:
responses:
"201":
description: Successful response
content:
application/json:
schema:
$ref: "#/components/schemas/OperationID"
default:
description: General Error
content:
@@ -1288,7 +1165,7 @@ paths:
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 0
responses:
"200":
@@ -1419,7 +1296,7 @@ paths:
in: query
schema:
type: integer
format: int32
format: uint32
minimum: 0
- name: ResourceID
in: query
@@ -1526,222 +1403,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/thumbnails/assets:
post:
summary: Batch fetch asset thumbnails
operationId: batchAssetThumbnails
tags:
- Thumbnails
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- assetIds
properties:
assetIds:
type: array
items:
type: integer
format: uint64
maxItems: 100
description: Array of asset IDs (max 100)
size:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "420x420"
description: Thumbnail size
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
thumbnails:
type: object
additionalProperties:
type: string
description: Map of asset ID to thumbnail URL
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/thumbnails/asset/{AssetID}:
get:
summary: Get single asset thumbnail
operationId: getAssetThumbnail
tags:
- Thumbnails
security: []
parameters:
- name: AssetID
in: path
required: true
schema:
type: integer
format: uint64
- name: size
in: query
schema:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "420x420"
responses:
"302":
description: Redirect to thumbnail URL
headers:
Location:
description: URL to redirect to
schema:
type: string
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/thumbnails/users:
post:
summary: Batch fetch user avatar thumbnails
operationId: batchUserThumbnails
tags:
- Thumbnails
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- userIds
properties:
userIds:
type: array
items:
type: integer
format: uint64
maxItems: 100
description: Array of user IDs (max 100)
size:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "150x150"
description: Thumbnail size
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
thumbnails:
type: object
additionalProperties:
type: string
description: Map of user ID to thumbnail URL
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/thumbnails/user/{UserID}:
get:
summary: Get single user avatar thumbnail
operationId: getUserThumbnail
tags:
- Thumbnails
security: []
parameters:
- name: UserID
in: path
required: true
schema:
type: integer
format: uint64
- name: size
in: query
schema:
type: string
enum:
- "150x150"
- "420x420"
- "768x432"
default: "150x150"
responses:
"302":
description: Redirect to thumbnail URL
headers:
Location:
description: URL to redirect to
schema:
type: string
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/usernames:
post:
summary: Batch fetch usernames
operationId: batchUsernames
tags:
- Users
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- userIds
properties:
userIds:
type: array
items:
type: integer
format: uint64
maxItems: 100
description: Array of user IDs (max 100)
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: object
properties:
usernames:
type: object
additionalProperties:
type: string
description: Map of user ID to username
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
securitySchemes:
cookieAuth:
@@ -1809,7 +1470,7 @@ components:
required: true
schema:
type: integer
format: int32
format: uint32
minimum: 1
Limit:
name: Limit
@@ -1817,7 +1478,7 @@ components:
required: true
schema:
type: integer
format: int32
format: uint32
minimum: 0
maximum: 100
schemas:
@@ -1841,20 +1502,20 @@ components:
format: int64
User:
type: integer
format: int64
format: uint64
Username:
type: string
maxLength: 64
ResourceType:
type: integer
format: int32
format: uint32
description: Is this a submission or is it a mapfix
ResourceID:
type: integer
format: int64
EventType:
type: integer
format: int32
format: uint32
EventData:
type: object
description: Arbitrary event data
@@ -1893,7 +1554,7 @@ components:
properties:
Roles:
type: integer
format: int32
format: uint32
minimum: 0
User:
required:
@@ -1904,7 +1565,7 @@ components:
properties:
UserID:
type: integer
format: int64
format: uint64
minimum: 0
Username:
type: string
@@ -1940,7 +1601,7 @@ components:
maxLength: 128
GameID:
type: integer
format: int32
format: uint32
minimum: 0
Date:
type: integer
@@ -1978,8 +1639,6 @@ components:
- Submitter
- AssetID
- AssetVersion
# - ValidatedAssetID
# - ValidatedAssetVersion
- Completed
- TargetAssetID
- StatusID
@@ -1998,7 +1657,7 @@ components:
maxLength: 128
GameID:
type: integer
format: int32
format: uint32
minimum: 0
CreatedAt:
type: integer
@@ -2010,33 +1669,25 @@ components:
minimum: 0
Submitter:
type: integer
format: int64
format: uint64
minimum: 0
AssetID:
type: integer
format: int64
format: uint64
minimum: 0
AssetVersion:
type: integer
format: int64
minimum: 0
ValidatedAssetID:
type: integer
format: int64
minimum: 0
ValidatedAssetVersion:
type: integer
format: int64
format: uint64
minimum: 0
Completed:
type: boolean
TargetAssetID:
type: integer
format: int64
format: uint64
minimum: 0
StatusID:
type: integer
format: int32
format: uint32
minimum: 0
Description:
type: string
@@ -2049,7 +1700,7 @@ components:
properties:
Total:
type: integer
format: int64
format: uint32
minimum: 0
Mapfixes:
type: array
@@ -2064,11 +1715,11 @@ components:
properties:
AssetID:
type: integer
format: int64
format: uint64
minimum: 0
TargetAssetID:
type: integer
format: int64
format: uint64
minimum: 0
Description:
type: string
@@ -2093,11 +1744,11 @@ components:
minimum: 0
Owner:
type: integer
format: int64
format: uint64
minimum: 0
Status:
type: integer
format: int32
format: uint32
minimum: 0
StatusMessage:
type: string
@@ -2135,7 +1786,7 @@ components:
maxLength: 128
GameID:
type: integer
format: int32
format: uint32
minimum: 0
CreatedAt:
type: integer
@@ -2147,33 +1798,33 @@ components:
minimum: 0
Submitter:
type: integer
format: int64
format: uint64
minimum: 0
AssetID:
type: integer
format: int64
format: uint64
minimum: 0
AssetVersion:
type: integer
format: int64
format: uint64
minimum: 0
ValidatedAssetID:
type: integer
format: int64
format: uint64
minimum: 0
ValidatedAssetVersion:
type: integer
format: int64
format: uint64
minimum: 0
Completed:
type: boolean
UploadedAssetID:
type: integer
format: int64
format: uint64
minimum: 0
StatusID:
type: integer
format: int32
format: uint32
minimum: 0
Submissions:
required:
@@ -2183,7 +1834,7 @@ components:
properties:
Total:
type: integer
format: int64
format: uint32
minimum: 0
Submissions:
type: array
@@ -2199,7 +1850,7 @@ components:
properties:
AssetID:
type: integer
format: int64
format: uint64
minimum: 0
DisplayName:
type: string
@@ -2209,7 +1860,7 @@ components:
maxLength: 128
GameID:
type: integer
format: int32
format: uint32
minimum: 0
ReleaseInfo:
required:
@@ -2250,7 +1901,7 @@ components:
maxLength: 1048576
ResourceType:
type: integer
format: int32
format: uint32
minimum: 0
ResourceID:
type: integer
@@ -2266,13 +1917,13 @@ components:
properties:
Name:
type: string
maxLength: 256
maxLength: 128
Source:
type: string
maxLength: 1048576
ResourceType:
type: integer
format: int32
format: uint32
minimum: 0
ResourceID:
type: integer
@@ -2295,7 +1946,7 @@ components:
maxLength: 1048576
ResourceType:
type: integer
format: int32
format: uint32
minimum: 0
ResourceID:
type: integer
@@ -2323,7 +1974,7 @@ components:
minimum: 0
Policy:
type: integer
format: int32
format: uint32
minimum: 0
ScriptPolicyCreate:
required:
@@ -2342,7 +1993,7 @@ components:
minimum: 0
Policy:
type: integer
format: int32
format: uint32
minimum: 0
ScriptPolicyUpdate:
required:
@@ -2363,56 +2014,15 @@ components:
minimum: 0
Policy:
type: integer
format: int32
format: uint32
minimum: 0
Stats:
description: Aggregate statistics for submissions and mapfixes
type: object
properties:
TotalSubmissions:
type: integer
format: int64
minimum: 0
description: Total number of submissions
TotalMapfixes:
type: integer
format: int64
minimum: 0
description: Total number of mapfixes
ReleasedSubmissions:
type: integer
format: int64
minimum: 0
description: Number of released submissions
ReleasedMapfixes:
type: integer
format: int64
minimum: 0
description: Number of released mapfixes
SubmittedSubmissions:
type: integer
format: int64
minimum: 0
description: Number of submissions under review
SubmittedMapfixes:
type: integer
format: int64
minimum: 0
description: Number of mapfixes under review
required:
- TotalSubmissions
- TotalMapfixes
- ReleasedSubmissions
- ReleasedMapfixes
- SubmittedSubmissions
- SubmittedMapfixes
Error:
description: Represents error object
type: object
properties:
code:
type: integer
format: int64
format: uint32
minimum: 0
message:
type: string

View File

@@ -4,16 +4,15 @@ package api
import (
"net/http"
"strings"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/otelogen"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
var (
@@ -33,7 +32,6 @@ type otelConfig struct {
Tracer trace.Tracer
MeterProvider metric.MeterProvider
Meter metric.Meter
Attributes []attribute.KeyValue
}
func (cfg *otelConfig) initOTEL() {
@@ -83,8 +81,18 @@ func (o otelOptionFunc) applyServer(c *serverConfig) {
func newServerConfig(opts ...ServerOption) serverConfig {
cfg := serverConfig{
NotFound: http.NotFound,
MethodNotAllowed: nil,
NotFound: http.NotFound,
MethodNotAllowed: func(w http.ResponseWriter, r *http.Request, allowed string) {
status := http.StatusMethodNotAllowed
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", allowed)
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
status = http.StatusNoContent
} else {
w.Header().Set("Allow", allowed)
}
w.WriteHeader(status)
},
ErrorHandler: ogenerrors.DefaultErrorHandler,
Middleware: nil,
MaxMultipartMemory: 32 << 20, // 32 MB
@@ -107,44 +115,8 @@ func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) {
s.cfg.NotFound(w, r)
}
type notAllowedParams struct {
allowedMethods string
allowedHeaders map[string]string
acceptPost string
acceptPatch string
}
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, params notAllowedParams) {
h := w.Header()
isOptions := r.Method == "OPTIONS"
if isOptions {
h.Set("Access-Control-Allow-Methods", params.allowedMethods)
if params.allowedHeaders != nil {
m := r.Header.Get("Access-Control-Request-Method")
if m != "" {
allowedHeaders, ok := params.allowedHeaders[strings.ToUpper(m)]
if ok {
h.Set("Access-Control-Allow-Headers", allowedHeaders)
}
}
}
if params.acceptPost != "" {
h.Set("Accept-Post", params.acceptPost)
}
if params.acceptPatch != "" {
h.Set("Accept-Patch", params.acceptPatch)
}
}
if s.cfg.MethodNotAllowed != nil {
s.cfg.MethodNotAllowed(w, r, params.allowedMethods)
return
}
status := http.StatusNoContent
if !isOptions {
h.Set("Allow", params.allowedMethods)
status = http.StatusMethodNotAllowed
}
w.WriteHeader(status)
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, allowed string) {
s.cfg.MethodNotAllowed(w, r, allowed)
}
func (cfg serverConfig) baseServer() (s baseServer, err error) {
@@ -243,13 +215,6 @@ func WithMeterProvider(provider metric.MeterProvider) Option {
})
}
// WithAttributes specifies default otel attributes.
func WithAttributes(attributes ...attribute.KeyValue) Option {
return otelOptionFunc(func(cfg *otelConfig) {
cfg.Attributes = attributes
})
}
// WithClient specifies http client to use.
func WithClient(client ht.Client) ClientOption {
return optionFunc[clientConfig](func(cfg *clientConfig) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package api
// setDefaults set default value of fields.
func (s *BatchAssetThumbnailsReq) setDefaults() {
{
val := BatchAssetThumbnailsReqSize("420x420")
s.Size.SetTo(val)
}
}
// setDefaults set default value of fields.
func (s *BatchUserThumbnailsReq) setDefaults() {
{
val := BatchUserThumbnailsReqSize("150x150")
s.Size.SetTo(val)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,10 @@ const (
ActionMapfixResetSubmittingOperation OperationName = "ActionMapfixResetSubmitting"
ActionMapfixRetryValidateOperation OperationName = "ActionMapfixRetryValidate"
ActionMapfixRevokeOperation OperationName = "ActionMapfixRevoke"
ActionMapfixTriggerReleaseOperation OperationName = "ActionMapfixTriggerRelease"
ActionMapfixTriggerSubmitOperation OperationName = "ActionMapfixTriggerSubmit"
ActionMapfixTriggerSubmitUncheckedOperation OperationName = "ActionMapfixTriggerSubmitUnchecked"
ActionMapfixTriggerUploadOperation OperationName = "ActionMapfixTriggerUpload"
ActionMapfixTriggerValidateOperation OperationName = "ActionMapfixTriggerValidate"
ActionMapfixUploadedOperation OperationName = "ActionMapfixUploaded"
ActionMapfixValidatedOperation OperationName = "ActionMapfixValidated"
ActionSubmissionAcceptedOperation OperationName = "ActionSubmissionAccepted"
ActionSubmissionRejectOperation OperationName = "ActionSubmissionReject"
@@ -30,10 +28,6 @@ const (
ActionSubmissionTriggerUploadOperation OperationName = "ActionSubmissionTriggerUpload"
ActionSubmissionTriggerValidateOperation OperationName = "ActionSubmissionTriggerValidate"
ActionSubmissionValidatedOperation OperationName = "ActionSubmissionValidated"
BatchAssetThumbnailsOperation OperationName = "BatchAssetThumbnails"
BatchUserThumbnailsOperation OperationName = "BatchUserThumbnails"
BatchUsernamesOperation OperationName = "BatchUsernames"
CombobulateMapOperation OperationName = "CombobulateMap"
CreateMapfixOperation OperationName = "CreateMapfix"
CreateMapfixAuditCommentOperation OperationName = "CreateMapfixAuditComment"
CreateScriptOperation OperationName = "CreateScript"
@@ -44,15 +38,12 @@ const (
DeleteScriptOperation OperationName = "DeleteScript"
DeleteScriptPolicyOperation OperationName = "DeleteScriptPolicy"
DownloadMapAssetOperation OperationName = "DownloadMapAsset"
GetAssetThumbnailOperation OperationName = "GetAssetThumbnail"
GetMapOperation OperationName = "GetMap"
GetMapfixOperation OperationName = "GetMapfix"
GetOperationOperation OperationName = "GetOperation"
GetScriptOperation OperationName = "GetScript"
GetScriptPolicyOperation OperationName = "GetScriptPolicy"
GetStatsOperation OperationName = "GetStats"
GetSubmissionOperation OperationName = "GetSubmission"
GetUserThumbnailOperation OperationName = "GetUserThumbnail"
ListMapfixAuditEventsOperation OperationName = "ListMapfixAuditEvents"
ListMapfixesOperation OperationName = "ListMapfixes"
ListMapsOperation OperationName = "ListMaps"
@@ -60,14 +51,13 @@ const (
ListScriptsOperation OperationName = "ListScripts"
ListSubmissionAuditEventsOperation OperationName = "ListSubmissionAuditEvents"
ListSubmissionsOperation OperationName = "ListSubmissions"
MigrateMapsOperation OperationName = "MigrateMaps"
ReleaseSubmissionsOperation OperationName = "ReleaseSubmissions"
SeedCombobulatorOperation OperationName = "SeedCombobulator"
SessionRolesOperation OperationName = "SessionRoles"
SessionUserOperation OperationName = "SessionUser"
SessionValidateOperation OperationName = "SessionValidate"
SetMapfixCompletedOperation OperationName = "SetMapfixCompleted"
SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted"
UpdateMapfixDescriptionOperation OperationName = "UpdateMapfixDescription"
UpdateMapfixModelOperation OperationName = "UpdateMapfixModel"
UpdateScriptOperation OperationName = "UpdateScript"
UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy"

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@
package api
import (
"bytes"
"fmt"
"io"
"mime"
@@ -11,250 +10,13 @@ import (
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/validate"
)
func (s *Server) decodeBatchAssetThumbnailsRequest(r *http.Request) (
req *BatchAssetThumbnailsReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request BatchAssetThumbnailsReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeBatchUserThumbnailsRequest(r *http.Request) (
req *BatchUserThumbnailsReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request BatchUserThumbnailsReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeBatchUsernamesRequest(r *http.Request) (
req *BatchUsernamesReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request BatchUsernamesReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
req *MapfixTriggerCreate,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -275,29 +37,22 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request MapfixTriggerCreate
@@ -315,7 +70,7 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
@@ -323,17 +78,16 @@ func (s *Server) decodeCreateMapfixRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
return &request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
req CreateMapfixAuditCommentReq,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -354,21 +108,20 @@ func (s *Server) decodeCreateMapfixAuditCommentRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "text/plain":
reader := r.Body
request := CreateMapfixAuditCommentReq{Data: reader}
return request, rawBody, close, nil
return request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateScriptRequest(r *http.Request) (
req *ScriptCreate,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -389,29 +142,22 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request ScriptCreate
@@ -429,7 +175,7 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
@@ -437,17 +183,16 @@ func (s *Server) decodeCreateScriptRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
return &request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
req *ScriptPolicyCreate,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -468,29 +213,22 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request ScriptPolicyCreate
@@ -508,7 +246,7 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
@@ -516,17 +254,16 @@ func (s *Server) decodeCreateScriptPolicyRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
return &request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
req *SubmissionTriggerCreate,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -547,29 +284,22 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request SubmissionTriggerCreate
@@ -587,7 +317,7 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
@@ -595,17 +325,16 @@ func (s *Server) decodeCreateSubmissionRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
return &request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
req *SubmissionTriggerCreate,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -626,29 +355,22 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request SubmissionTriggerCreate
@@ -666,7 +388,7 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
@@ -674,17 +396,16 @@ func (s *Server) decodeCreateSubmissionAdminRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
return &request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
req CreateSubmissionAuditCommentReq,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -705,21 +426,20 @@ func (s *Server) decodeCreateSubmissionAuditCommentRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "text/plain":
reader := r.Body
request := CreateSubmissionAuditCommentReq{Data: reader}
return request, rawBody, close, nil
return request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
req []ReleaseInfo,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -740,29 +460,22 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request []ReleaseInfo
@@ -788,7 +501,7 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if request == nil {
@@ -821,52 +534,16 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return request, rawBody, close, nil
return request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeUpdateMapfixDescriptionRequest(r *http.Request) (
req UpdateMapfixDescriptionReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "text/plain":
reader := r.Body
request := UpdateMapfixDescriptionReq{Data: reader}
return request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
req *ScriptUpdate,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -887,29 +564,22 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request ScriptUpdate
@@ -927,7 +597,7 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
@@ -935,17 +605,16 @@ func (s *Server) decodeUpdateScriptRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
return &request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
req *ScriptPolicyUpdate,
rawBody []byte,
close func() error,
rerr error,
) {
@@ -966,29 +635,22 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
return req, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
return req, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request ScriptPolicyUpdate
@@ -1006,7 +668,7 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
Body: buf,
Err: err,
}
return req, rawBody, close, err
return req, close, err
}
if err := func() error {
if err := request.Validate(); err != nil {
@@ -1014,10 +676,10 @@ func (s *Server) decodeUpdateScriptPolicyRequest(r *http.Request) (
}
return nil
}(); err != nil {
return req, rawBody, close, errors.Wrap(err, "validate")
return req, close, errors.Wrap(err, "validate")
}
return &request, rawBody, close, nil
return &request, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
return req, close, validate.InvalidContentType(ct)
}
}

View File

@@ -7,51 +7,10 @@ import (
"net/http"
"github.com/go-faster/jx"
ht "github.com/ogen-go/ogen/http"
)
func encodeBatchAssetThumbnailsRequest(
req *BatchAssetThumbnailsReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeBatchUserThumbnailsRequest(
req *BatchUserThumbnailsReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeBatchUsernamesRequest(
req *BatchUsernamesReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeCreateMapfixRequest(
req *MapfixTriggerCreate,
r *http.Request,
@@ -160,16 +119,6 @@ func encodeReleaseSubmissionsRequest(
return nil
}
func encodeUpdateMapfixDescriptionRequest(
req UpdateMapfixDescriptionReq,
r *http.Request,
) error {
const contentType = "text/plain"
body := req
ht.SetBody(r, body, contentType)
return nil
}
func encodeUpdateScriptRequest(
req *ScriptUpdate,
r *http.Request,

View File

@@ -11,9 +11,8 @@ import (
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/conv"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/uri"
"github.com/ogen-go/ogen/validate"
)
@@ -377,66 +376,6 @@ func decodeActionMapfixRevokeResponse(resp *http.Response) (res *ActionMapfixRev
return res, errors.Wrap(defRes, "error")
}
func decodeActionMapfixTriggerReleaseResponse(resp *http.Response) (res *ActionMapfixTriggerReleaseNoContent, _ error) {
switch resp.StatusCode {
case 204:
// Code 204.
return &ActionMapfixTriggerReleaseNoContent{}, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeActionMapfixTriggerSubmitResponse(resp *http.Response) (res *ActionMapfixTriggerSubmitNoContent, _ error) {
switch resp.StatusCode {
case 204:
@@ -677,66 +616,6 @@ func decodeActionMapfixTriggerValidateResponse(resp *http.Response) (res *Action
return res, errors.Wrap(defRes, "error")
}
func decodeActionMapfixUploadedResponse(resp *http.Response) (res *ActionMapfixUploadedNoContent, _ error) {
switch resp.StatusCode {
case 204:
// Code 204.
return &ActionMapfixUploadedNoContent{}, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeActionMapfixValidatedResponse(resp *http.Response) (res *ActionMapfixValidatedNoContent, _ error) {
switch resp.StatusCode {
case 204:
@@ -1457,342 +1336,6 @@ func decodeActionSubmissionValidatedResponse(resp *http.Response) (res *ActionSu
return res, errors.Wrap(defRes, "error")
}
func decodeBatchAssetThumbnailsResponse(resp *http.Response) (res *BatchAssetThumbnailsOK, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response BatchAssetThumbnailsOK
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
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeBatchUserThumbnailsResponse(resp *http.Response) (res *BatchUserThumbnailsOK, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response BatchUserThumbnailsOK
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
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeBatchUsernamesResponse(resp *http.Response) (res *BatchUsernamesOK, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response BatchUsernamesOK
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
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func 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:
@@ -2614,105 +2157,6 @@ func decodeDownloadMapAssetResponse(resp *http.Response) (res DownloadMapAssetOK
return res, errors.Wrap(defRes, "error")
}
func decodeGetAssetThumbnailResponse(resp *http.Response) (res *GetAssetThumbnailFound, _ error) {
switch resp.StatusCode {
case 302:
// Code 302.
var wrapper GetAssetThumbnailFound
h := uri.NewHeaderDecoder(resp.Header)
// Parse "Location" header.
{
cfg := uri.HeaderParameterDecodingConfig{
Name: "Location",
Explode: false,
}
if err := func() error {
if err := h.HasParam(cfg); err == nil {
if err := h.DecodeParam(cfg, func(d uri.Decoder) error {
var wrapperDotLocationVal string
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToString(val)
if err != nil {
return err
}
wrapperDotLocationVal = c
return nil
}(); err != nil {
return err
}
wrapper.Location.SetTo(wrapperDotLocationVal)
return nil
}); err != nil {
return err
}
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "parse Location header")
}
}
return &wrapper, 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 decodeGetMapResponse(resp *http.Response) (res *Map, _ error) {
switch resp.StatusCode {
case 200:
@@ -3218,107 +2662,6 @@ func decodeGetScriptPolicyResponse(resp *http.Response) (res *ScriptPolicy, _ er
return res, errors.Wrap(defRes, "error")
}
func decodeGetStatsResponse(resp *http.Response) (res *Stats, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Stats
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error) {
switch resp.StatusCode {
case 200:
@@ -3420,105 +2763,6 @@ func decodeGetSubmissionResponse(resp *http.Response) (res *Submission, _ error)
return res, errors.Wrap(defRes, "error")
}
func decodeGetUserThumbnailResponse(resp *http.Response) (res *GetUserThumbnailFound, _ error) {
switch resp.StatusCode {
case 302:
// Code 302.
var wrapper GetUserThumbnailFound
h := uri.NewHeaderDecoder(resp.Header)
// Parse "Location" header.
{
cfg := uri.HeaderParameterDecodingConfig{
Name: "Location",
Explode: false,
}
if err := func() error {
if err := h.HasParam(cfg); err == nil {
if err := h.DecodeParam(cfg, func(d uri.Decoder) error {
var wrapperDotLocationVal string
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToString(val)
if err != nil {
return err
}
wrapperDotLocationVal = c
return nil
}(); err != nil {
return err
}
wrapper.Location.SetTo(wrapperDotLocationVal)
return nil
}); err != nil {
return err
}
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "parse Location header")
}
}
return &wrapper, 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 decodeListMapfixAuditEventsResponse(resp *http.Response) (res []AuditEvent, _ error) {
switch resp.StatusCode {
case 200:
@@ -4351,52 +3595,11 @@ func decodeListSubmissionsResponse(resp *http.Response) (res *Submissions, _ err
return res, errors.Wrap(defRes, "error")
}
func decodeReleaseSubmissionsResponse(resp *http.Response) (res *OperationID, _ error) {
func decodeMigrateMapsResponse(resp *http.Response) (res *MigrateMapsOK, _ error) {
switch resp.StatusCode {
case 201:
// Code 201.
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 OperationID
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
case 200:
// Code 200.
return &MigrateMapsOK{}, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
@@ -4452,11 +3655,11 @@ func decodeReleaseSubmissionsResponse(resp *http.Response) (res *OperationID, _
return res, errors.Wrap(defRes, "error")
}
func decodeSeedCombobulatorResponse(resp *http.Response) (res *SeedCombobulatorNoContent, _ error) {
func decodeReleaseSubmissionsResponse(resp *http.Response) (res *ReleaseSubmissionsCreated, _ error) {
switch resp.StatusCode {
case 204:
// Code 204.
return &SeedCombobulatorNoContent{}, nil
case 201:
// Code 201.
return &ReleaseSubmissionsCreated{}, nil
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
@@ -4928,66 +4131,6 @@ func decodeSetSubmissionCompletedResponse(resp *http.Response) (res *SetSubmissi
return res, errors.Wrap(defRes, "error")
}
func decodeUpdateMapfixDescriptionResponse(resp *http.Response) (res *UpdateMapfixDescriptionNoContent, _ error) {
switch resp.StatusCode {
case 204:
// Code 204.
return &UpdateMapfixDescriptionNoContent{}, 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 decodeUpdateMapfixModelResponse(resp *http.Response) (res *UpdateMapfixModelNoContent, _ error) {
switch resp.StatusCode {
case 204:

View File

@@ -8,11 +8,10 @@ import (
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/conv"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/uri"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
ht "github.com/ogen-go/ogen/http"
)
func encodeActionMapfixAcceptedResponse(response *ActionMapfixAcceptedNoContent, w http.ResponseWriter, span trace.Span) error {
@@ -57,13 +56,6 @@ func encodeActionMapfixRevokeResponse(response *ActionMapfixRevokeNoContent, w h
return nil
}
func encodeActionMapfixTriggerReleaseResponse(response *ActionMapfixTriggerReleaseNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))
return nil
}
func encodeActionMapfixTriggerSubmitResponse(response *ActionMapfixTriggerSubmitNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))
@@ -92,13 +84,6 @@ func encodeActionMapfixTriggerValidateResponse(response *ActionMapfixTriggerVali
return nil
}
func encodeActionMapfixUploadedResponse(response *ActionMapfixUploadedNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))
return nil
}
func encodeActionMapfixValidatedResponse(response *ActionMapfixValidatedNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))
@@ -183,55 +168,6 @@ func encodeActionSubmissionValidatedResponse(response *ActionSubmissionValidated
return nil
}
func encodeBatchAssetThumbnailsResponse(response *BatchAssetThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeBatchUserThumbnailsResponse(response *BatchUserThumbnailsOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeBatchUsernamesResponse(response *BatchUsernamesOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func 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)
@@ -346,33 +282,6 @@ func encodeDownloadMapAssetResponse(response DownloadMapAssetOK, w http.Response
return nil
}
func encodeGetAssetThumbnailResponse(response *GetAssetThumbnailFound, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Access-Control-Expose-Headers", "Location")
// Encoding response headers.
{
h := uri.NewHeaderEncoder(w.Header())
// Encode "Location" header.
{
cfg := uri.HeaderParameterEncodingConfig{
Name: "Location",
Explode: false,
}
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
if val, ok := response.Location.Get(); ok {
return e.EncodeValue(conv.StringToString(val))
}
return nil
}); err != nil {
return errors.Wrap(err, "encode Location header")
}
}
}
w.WriteHeader(302)
span.SetStatus(codes.Ok, http.StatusText(302))
return nil
}
func encodeGetMapResponse(response *Map, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
@@ -443,20 +352,6 @@ func encodeGetScriptPolicyResponse(response *ScriptPolicy, w http.ResponseWriter
return nil
}
func encodeGetStatsResponse(response *Stats, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
@@ -471,33 +366,6 @@ func encodeGetSubmissionResponse(response *Submission, w http.ResponseWriter, sp
return nil
}
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())
// Encode "Location" header.
{
cfg := uri.HeaderParameterEncodingConfig{
Name: "Location",
Explode: false,
}
if err := h.EncodeParam(cfg, func(e uri.Encoder) error {
if val, ok := response.Location.Get(); ok {
return e.EncodeValue(conv.StringToString(val))
}
return nil
}); err != nil {
return errors.Wrap(err, "encode Location header")
}
}
}
w.WriteHeader(302)
span.SetStatus(codes.Ok, http.StatusText(302))
return nil
}
func encodeListMapfixAuditEventsResponse(response []AuditEvent, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
@@ -616,23 +484,16 @@ func encodeListSubmissionsResponse(response *Submissions, w http.ResponseWriter,
return nil
}
func encodeReleaseSubmissionsResponse(response *OperationID, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(201)
span.SetStatus(codes.Ok, http.StatusText(201))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
func encodeMigrateMapsResponse(response *MigrateMapsOK, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
return nil
}
func encodeSeedCombobulatorResponse(response *SeedCombobulatorNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))
func encodeReleaseSubmissionsResponse(response *ReleaseSubmissionsCreated, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(201)
span.SetStatus(codes.Ok, http.StatusText(201))
return nil
}
@@ -693,13 +554,6 @@ func encodeSetSubmissionCompletedResponse(response *SetSubmissionCompletedNoCont
return nil
}
func encodeUpdateMapfixDescriptionResponse(response *UpdateMapfixDescriptionNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))
return nil
}
func encodeUpdateMapfixModelResponse(response *UpdateMapfixModelNoContent, w http.ResponseWriter, span trace.Span) error {
w.WriteHeader(204)
span.SetStatus(codes.Ok, http.StatusText(204))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/go-faster/errors"
"github.com/ogen-go/ogen/ogenerrors"
)
@@ -32,7 +33,6 @@ func findAuthorization(h http.Header, prefix string) (string, bool) {
return "", false
}
// operationRolesCookieAuth is a private map storing roles per operation.
var operationRolesCookieAuth = map[string][]string{
ActionMapfixAcceptedOperation: []string{},
ActionMapfixRejectOperation: []string{},
@@ -40,12 +40,10 @@ var operationRolesCookieAuth = map[string][]string{
ActionMapfixResetSubmittingOperation: []string{},
ActionMapfixRetryValidateOperation: []string{},
ActionMapfixRevokeOperation: []string{},
ActionMapfixTriggerReleaseOperation: []string{},
ActionMapfixTriggerSubmitOperation: []string{},
ActionMapfixTriggerSubmitUncheckedOperation: []string{},
ActionMapfixTriggerUploadOperation: []string{},
ActionMapfixTriggerValidateOperation: []string{},
ActionMapfixUploadedOperation: []string{},
ActionMapfixValidatedOperation: []string{},
ActionSubmissionAcceptedOperation: []string{},
ActionSubmissionRejectOperation: []string{},
@@ -58,7 +56,6 @@ var operationRolesCookieAuth = map[string][]string{
ActionSubmissionTriggerUploadOperation: []string{},
ActionSubmissionTriggerValidateOperation: []string{},
ActionSubmissionValidatedOperation: []string{},
CombobulateMapOperation: []string{},
CreateMapfixOperation: []string{},
CreateMapfixAuditCommentOperation: []string{},
CreateScriptOperation: []string{},
@@ -70,41 +67,19 @@ var operationRolesCookieAuth = map[string][]string{
DeleteScriptPolicyOperation: []string{},
DownloadMapAssetOperation: []string{},
GetOperationOperation: []string{},
MigrateMapsOperation: []string{},
ReleaseSubmissionsOperation: []string{},
SeedCombobulatorOperation: []string{},
SessionRolesOperation: []string{},
SessionUserOperation: []string{},
SessionValidateOperation: []string{},
SetMapfixCompletedOperation: []string{},
SetSubmissionCompletedOperation: []string{},
UpdateMapfixDescriptionOperation: []string{},
UpdateMapfixModelOperation: []string{},
UpdateScriptOperation: []string{},
UpdateScriptPolicyOperation: []string{},
UpdateSubmissionModelOperation: []string{},
}
// GetRolesForCookieAuth returns the required roles for the given operation.
//
// This is useful for authorization scenarios where you need to know which roles
// are required for an operation.
//
// Example:
//
// requiredRoles := GetRolesForCookieAuth(AddPetOperation)
//
// Returns nil if the operation has no role requirements or if the operation is unknown.
func GetRolesForCookieAuth(operation string) []string {
roles, ok := operationRolesCookieAuth[operation]
if !ok {
return nil
}
// Return a copy to prevent external modification
result := make([]string, len(roles))
copy(result, roles)
return result
}
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

@@ -45,12 +45,6 @@ type Handler interface {
//
// POST /mapfixes/{MapfixID}/status/revoke
ActionMapfixRevoke(ctx context.Context, params ActionMapfixRevokeParams) error
// ActionMapfixTriggerRelease implements actionMapfixTriggerRelease operation.
//
// Role MapfixUpload changes status from Uploaded -> Releasing.
//
// POST /mapfixes/{MapfixID}/status/trigger-release
ActionMapfixTriggerRelease(ctx context.Context, params ActionMapfixTriggerReleaseParams) error
// ActionMapfixTriggerSubmit implements actionMapfixTriggerSubmit operation.
//
// Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting.
@@ -65,7 +59,7 @@ type Handler interface {
ActionMapfixTriggerSubmitUnchecked(ctx context.Context, params ActionMapfixTriggerSubmitUncheckedParams) error
// ActionMapfixTriggerUpload implements actionMapfixTriggerUpload operation.
//
// Role MapfixUpload changes status from Validated -> Uploading.
// Role Admin changes status from Validated -> Uploading.
//
// POST /mapfixes/{MapfixID}/status/trigger-upload
ActionMapfixTriggerUpload(ctx context.Context, params ActionMapfixTriggerUploadParams) error
@@ -75,15 +69,9 @@ type Handler interface {
//
// POST /mapfixes/{MapfixID}/status/trigger-validate
ActionMapfixTriggerValidate(ctx context.Context, params ActionMapfixTriggerValidateParams) error
// ActionMapfixUploaded implements actionMapfixUploaded operation.
//
// Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded.
//
// POST /mapfixes/{MapfixID}/status/reset-releasing
ActionMapfixUploaded(ctx context.Context, params ActionMapfixUploadedParams) error
// ActionMapfixValidated implements actionMapfixValidated operation.
//
// Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated.
// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated.
//
// POST /mapfixes/{MapfixID}/status/reset-uploading
ActionMapfixValidated(ctx context.Context, params ActionMapfixValidatedParams) error
@@ -138,7 +126,7 @@ type Handler interface {
ActionSubmissionTriggerSubmitUnchecked(ctx context.Context, params ActionSubmissionTriggerSubmitUncheckedParams) error
// ActionSubmissionTriggerUpload implements actionSubmissionTriggerUpload operation.
//
// Role SubmissionUpload changes status from Validated -> Uploading.
// Role Admin changes status from Validated -> Uploading.
//
// POST /submissions/{SubmissionID}/status/trigger-upload
ActionSubmissionTriggerUpload(ctx context.Context, params ActionSubmissionTriggerUploadParams) error
@@ -150,35 +138,10 @@ type Handler interface {
ActionSubmissionTriggerValidate(ctx context.Context, params ActionSubmissionTriggerValidateParams) error
// ActionSubmissionValidated implements actionSubmissionValidated operation.
//
// Role SubmissionUpload manually resets uploading softlock and changes status from Uploading ->
// Validated.
// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated.
//
// POST /submissions/{SubmissionID}/status/reset-uploading
ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error
// BatchAssetThumbnails implements batchAssetThumbnails operation.
//
// Batch fetch asset thumbnails.
//
// POST /thumbnails/assets
BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (*BatchAssetThumbnailsOK, error)
// BatchUserThumbnails implements batchUserThumbnails operation.
//
// Batch fetch user avatar thumbnails.
//
// POST /thumbnails/users
BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (*BatchUserThumbnailsOK, error)
// BatchUsernames implements batchUsernames operation.
//
// Batch fetch usernames.
//
// POST /usernames
BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (*BatchUsernamesOK, error)
// 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.
@@ -239,12 +202,6 @@ type Handler interface {
//
// GET /maps/{MapID}/download
DownloadMapAsset(ctx context.Context, params DownloadMapAssetParams) (DownloadMapAssetOK, error)
// GetAssetThumbnail implements getAssetThumbnail operation.
//
// Get single asset thumbnail.
//
// GET /thumbnails/asset/{AssetID}
GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (*GetAssetThumbnailFound, error)
// GetMap implements getMap operation.
//
// Retrieve map with ID.
@@ -275,24 +232,12 @@ type Handler interface {
//
// GET /script-policy/{ScriptPolicyID}
GetScriptPolicy(ctx context.Context, params GetScriptPolicyParams) (*ScriptPolicy, error)
// GetStats implements getStats operation.
//
// Get aggregate statistics.
//
// GET /stats
GetStats(ctx context.Context) (*Stats, error)
// GetSubmission implements getSubmission operation.
//
// Retrieve map with ID.
//
// GET /submissions/{SubmissionID}
GetSubmission(ctx context.Context, params GetSubmissionParams) (*Submission, error)
// GetUserThumbnail implements getUserThumbnail operation.
//
// Get single user avatar thumbnail.
//
// GET /thumbnails/user/{UserID}
GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (*GetUserThumbnailFound, error)
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
//
// Retrieve a list of audit events.
@@ -335,18 +280,18 @@ type Handler interface {
//
// GET /submissions
ListSubmissions(ctx context.Context, params ListSubmissionsParams) (*Submissions, error)
// MigrateMaps implements migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate-maps
MigrateMaps(ctx context.Context) error
// ReleaseSubmissions implements releaseSubmissions operation.
//
// Release a set of uploaded maps. Role SubmissionRelease.
// Release a set of uploaded maps.
//
// 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
ReleaseSubmissions(ctx context.Context, req []ReleaseInfo) error
// SessionRoles implements sessionRoles operation.
//
// Get list of roles for the current session.
@@ -377,12 +322,6 @@ type Handler interface {
//
// POST /submissions/{SubmissionID}/completed
SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error
// UpdateMapfixDescription implements updateMapfixDescription operation.
//
// Update description (submitter only).
//
// PATCH /mapfixes/{MapfixID}/description
UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error
// UpdateMapfixModel implements updateMapfixModel operation.
//
// Update model following role restrictions.

View File

@@ -68,15 +68,6 @@ func (UnimplementedHandler) ActionMapfixRevoke(ctx context.Context, params Actio
return ht.ErrNotImplemented
}
// ActionMapfixTriggerRelease implements actionMapfixTriggerRelease operation.
//
// Role MapfixUpload changes status from Uploaded -> Releasing.
//
// POST /mapfixes/{MapfixID}/status/trigger-release
func (UnimplementedHandler) ActionMapfixTriggerRelease(ctx context.Context, params ActionMapfixTriggerReleaseParams) error {
return ht.ErrNotImplemented
}
// ActionMapfixTriggerSubmit implements actionMapfixTriggerSubmit operation.
//
// Role Submitter changes status from UnderConstruction|ChangesRequested -> Submitting.
@@ -97,7 +88,7 @@ func (UnimplementedHandler) ActionMapfixTriggerSubmitUnchecked(ctx context.Conte
// ActionMapfixTriggerUpload implements actionMapfixTriggerUpload operation.
//
// Role MapfixUpload changes status from Validated -> Uploading.
// Role Admin changes status from Validated -> Uploading.
//
// POST /mapfixes/{MapfixID}/status/trigger-upload
func (UnimplementedHandler) ActionMapfixTriggerUpload(ctx context.Context, params ActionMapfixTriggerUploadParams) error {
@@ -113,18 +104,9 @@ func (UnimplementedHandler) ActionMapfixTriggerValidate(ctx context.Context, par
return ht.ErrNotImplemented
}
// ActionMapfixUploaded implements actionMapfixUploaded operation.
//
// Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded.
//
// POST /mapfixes/{MapfixID}/status/reset-releasing
func (UnimplementedHandler) ActionMapfixUploaded(ctx context.Context, params ActionMapfixUploadedParams) error {
return ht.ErrNotImplemented
}
// ActionMapfixValidated implements actionMapfixValidated operation.
//
// Role MapfixUpload manually resets uploading softlock and changes status from Uploading -> Validated.
// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated.
//
// POST /mapfixes/{MapfixID}/status/reset-uploading
func (UnimplementedHandler) ActionMapfixValidated(ctx context.Context, params ActionMapfixValidatedParams) error {
@@ -206,7 +188,7 @@ func (UnimplementedHandler) ActionSubmissionTriggerSubmitUnchecked(ctx context.C
// ActionSubmissionTriggerUpload implements actionSubmissionTriggerUpload operation.
//
// Role SubmissionUpload changes status from Validated -> Uploading.
// Role Admin changes status from Validated -> Uploading.
//
// POST /submissions/{SubmissionID}/status/trigger-upload
func (UnimplementedHandler) ActionSubmissionTriggerUpload(ctx context.Context, params ActionSubmissionTriggerUploadParams) error {
@@ -224,50 +206,13 @@ func (UnimplementedHandler) ActionSubmissionTriggerValidate(ctx context.Context,
// ActionSubmissionValidated implements actionSubmissionValidated operation.
//
// Role SubmissionUpload manually resets uploading softlock and changes status from Uploading ->
// Validated.
// Role Admin manually resets uploading softlock and changes status from Uploading -> Validated.
//
// POST /submissions/{SubmissionID}/status/reset-uploading
func (UnimplementedHandler) ActionSubmissionValidated(ctx context.Context, params ActionSubmissionValidatedParams) error {
return ht.ErrNotImplemented
}
// BatchAssetThumbnails implements batchAssetThumbnails operation.
//
// Batch fetch asset thumbnails.
//
// POST /thumbnails/assets
func (UnimplementedHandler) BatchAssetThumbnails(ctx context.Context, req *BatchAssetThumbnailsReq) (r *BatchAssetThumbnailsOK, _ error) {
return r, ht.ErrNotImplemented
}
// BatchUserThumbnails implements batchUserThumbnails operation.
//
// Batch fetch user avatar thumbnails.
//
// POST /thumbnails/users
func (UnimplementedHandler) BatchUserThumbnails(ctx context.Context, req *BatchUserThumbnailsReq) (r *BatchUserThumbnailsOK, _ error) {
return r, ht.ErrNotImplemented
}
// BatchUsernames implements batchUsernames operation.
//
// Batch fetch usernames.
//
// POST /usernames
func (UnimplementedHandler) BatchUsernames(ctx context.Context, req *BatchUsernamesReq) (r *BatchUsernamesOK, _ error) {
return r, ht.ErrNotImplemented
}
// 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.
@@ -358,15 +303,6 @@ func (UnimplementedHandler) DownloadMapAsset(ctx context.Context, params Downloa
return r, ht.ErrNotImplemented
}
// GetAssetThumbnail implements getAssetThumbnail operation.
//
// Get single asset thumbnail.
//
// GET /thumbnails/asset/{AssetID}
func (UnimplementedHandler) GetAssetThumbnail(ctx context.Context, params GetAssetThumbnailParams) (r *GetAssetThumbnailFound, _ error) {
return r, ht.ErrNotImplemented
}
// GetMap implements getMap operation.
//
// Retrieve map with ID.
@@ -412,15 +348,6 @@ func (UnimplementedHandler) GetScriptPolicy(ctx context.Context, params GetScrip
return r, ht.ErrNotImplemented
}
// GetStats implements getStats operation.
//
// Get aggregate statistics.
//
// GET /stats
func (UnimplementedHandler) GetStats(ctx context.Context) (r *Stats, _ error) {
return r, ht.ErrNotImplemented
}
// GetSubmission implements getSubmission operation.
//
// Retrieve map with ID.
@@ -430,15 +357,6 @@ func (UnimplementedHandler) GetSubmission(ctx context.Context, params GetSubmiss
return r, ht.ErrNotImplemented
}
// GetUserThumbnail implements getUserThumbnail operation.
//
// Get single user avatar thumbnail.
//
// GET /thumbnails/user/{UserID}
func (UnimplementedHandler) GetUserThumbnail(ctx context.Context, params GetUserThumbnailParams) (r *GetUserThumbnailFound, _ error) {
return r, ht.ErrNotImplemented
}
// ListMapfixAuditEvents implements listMapfixAuditEvents operation.
//
// Retrieve a list of audit events.
@@ -502,21 +420,21 @@ func (UnimplementedHandler) ListSubmissions(ctx context.Context, params ListSubm
return r, ht.ErrNotImplemented
}
// ReleaseSubmissions implements releaseSubmissions operation.
// MigrateMaps implements migrateMaps operation.
//
// Release a set of uploaded maps. Role SubmissionRelease.
// Perform maps migration.
//
// POST /release-submissions
func (UnimplementedHandler) ReleaseSubmissions(ctx context.Context, req []ReleaseInfo) (r *OperationID, _ error) {
return r, ht.ErrNotImplemented
// POST /migrate-maps
func (UnimplementedHandler) MigrateMaps(ctx context.Context) error {
return ht.ErrNotImplemented
}
// SeedCombobulator implements seedCombobulator operation.
// ReleaseSubmissions implements releaseSubmissions operation.
//
// Queue all maps for combobulator processing.
// Release a set of uploaded maps.
//
// POST /maps-admin/seed-combobulator
func (UnimplementedHandler) SeedCombobulator(ctx context.Context) error {
// POST /release-submissions
func (UnimplementedHandler) ReleaseSubmissions(ctx context.Context, req []ReleaseInfo) error {
return ht.ErrNotImplemented
}
@@ -565,15 +483,6 @@ func (UnimplementedHandler) SetSubmissionCompleted(ctx context.Context, params S
return ht.ErrNotImplemented
}
// UpdateMapfixDescription implements updateMapfixDescription operation.
//
// Update description (submitter only).
//
// PATCH /mapfixes/{MapfixID}/description
func (UnimplementedHandler) UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error {
return ht.ErrNotImplemented
}
// UpdateMapfixModel implements updateMapfixModel operation.
//
// Update model following role restrictions.

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
package cmds
import (
"git.itzana.me/strafesnet/maps-service/pkg/public_api"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func NewApiCommand() *cli.Command {
return &cli.Command{
Name: "api",
Usage: "Run api service",
Action: runAPI,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "port",
Usage: "Listen port",
EnvVars: []string{"PORT"},
Value: 8080,
},
&cli.StringFlag{
Name: "dev-rpc-host",
Usage: "Host of dev rpc",
EnvVars: []string{"DEV_RPC_HOST"},
Value: "dev-service:8081",
},
&cli.StringFlag{
Name: "maps-rpc-host",
Usage: "Host of maps rpc",
EnvVars: []string{"MAPS_RPC_HOST"},
Value: "maptest-api:8081",
},
},
}
}
func runAPI(ctx *cli.Context) error {
// Dev service client
devConn, err := grpc.Dial(ctx.String("dev-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return err
}
// Data service client
mapsConn, err := grpc.Dial(ctx.String("maps-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return err
}
return api.NewRouter(
api.WithContext(ctx),
api.WithPort(ctx.Int("port")),
api.WithDevClient(devConn),
api.WithMapsClient(mapsConn),
)
}

View File

@@ -8,8 +8,6 @@ import (
"git.itzana.me/strafesnet/go-grpc/auth"
"git.itzana.me/strafesnet/go-grpc/maps"
"git.itzana.me/strafesnet/go-grpc/maps_extended"
"git.itzana.me/strafesnet/go-grpc/mapfixes"
"git.itzana.me/strafesnet/go-grpc/submissions"
"git.itzana.me/strafesnet/go-grpc/users"
"git.itzana.me/strafesnet/go-grpc/validator"
"git.itzana.me/strafesnet/maps-service/pkg/api"
@@ -19,10 +17,7 @@ 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"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
@@ -107,30 +102,6 @@ func NewServeCommand() *cli.Command {
EnvVars: []string{"RBX_API_KEY"},
Required: true,
},
&cli.StringFlag{
Name: "redis-host",
Usage: "Host of Redis cache",
EnvVars: []string{"REDIS_HOST"},
Value: "localhost:6379",
},
&cli.StringFlag{
Name: "redis-password",
Usage: "Password for Redis",
EnvVars: []string{"REDIS_PASSWORD"},
Value: "",
},
&cli.IntFlag{
Name: "redis-db",
Usage: "Redis database number",
EnvVars: []string{"REDIS_DB"},
Value: 0,
},
&cli.StringFlag{
Name: "s3-bucket",
Usage: "S3 bucket for map assets",
EnvVars: []string{"S3_BUCKET"},
Required: true,
},
},
}
}
@@ -152,37 +123,12 @@ func serve(ctx *cli.Context) error {
_, err = js.AddStream(&nats.StreamConfig{
Name: "maptest",
Subjects: []string{"maptest.>"},
Retention: nats.InterestPolicy,
Retention: nats.WorkQueuePolicy,
})
if err != nil {
log.WithError(err).Fatal("failed to add stream")
}
// Initialize Redis client
redisClient := redis.NewClient(&redis.Options{
Addr: ctx.String("redis-host"),
Password: ctx.String("redis-password"),
DB: ctx.Int("redis-db"),
})
// Test Redis connection
if err := redisClient.Ping(ctx.Context).Err(); err != nil {
log.WithError(err).Warn("failed to connect to Redis - thumbnails will not be cached")
}
// Initialize Roblox client
robloxClient := &roblox.Client{
HttpClient: http.DefaultClient,
ApiKey: ctx.String("rbx-api-key"),
}
// 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 {
@@ -193,17 +139,13 @@ func serve(ctx *cli.Context) error {
js,
maps.NewMapsServiceClient(conn),
users.NewUsersServiceClient(conn),
robloxClient,
redisClient,
s3Client,
ctx.String("s3-bucket"),
)
svc_external := web_api.NewService(
&svc_inner,
roblox.Client{
HttpClient: http.DefaultClient,
ApiKey: ctx.String("rbx-api-key"),
ApiKey: ctx.String("rbx-api-key"),
},
)
@@ -223,11 +165,7 @@ func serve(ctx *cli.Context) error {
grpcServer := grpc.NewServer()
maps_controller := controller.NewMapsController(&svc_inner)
mapfixes_controller := controller.NewMapfixesController(&svc_inner)
submissions_controller := controller.NewSubmissionsController(&svc_inner)
maps_extended.RegisterMapsServiceServer(grpcServer,&maps_controller)
mapfixes.RegisterMapfixesServiceServer(grpcServer,&mapfixes_controller)
submissions.RegisterSubmissionsServiceServer(grpcServer,&submissions_controller)
mapfix_controller := validator_controller.NewMapfixesController(&svc_inner)
operation_controller := validator_controller.NewOperationsController(&svc_inner)

View File

@@ -1,149 +0,0 @@
package controller
import (
"context"
"git.itzana.me/strafesnet/go-grpc/mapfixes"
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
"git.itzana.me/strafesnet/maps-service/pkg/model"
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
type Mapfixes struct {
*mapfixes.UnimplementedMapfixesServiceServer
inner *service.Service
}
func NewMapfixesController(
inner *service.Service,
) Mapfixes {
return Mapfixes{
inner: inner,
}
}
func (svc *Mapfixes) Get(ctx context.Context, request *mapfixes.MapfixId) (*mapfixes.MapfixResponse, error) {
item, err := svc.inner.GetMapfix(ctx, request.ID)
if err != nil {
return nil, err
}
var validated_asset_id *uint64
if item.ValidatedAssetID != 0 {
validated_asset_id = &item.ValidatedAssetID
}
var validated_asset_version *uint64
if item.ValidatedAssetVersion != 0 {
validated_asset_version = &item.ValidatedAssetVersion
}
return &mapfixes.MapfixResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
AssetVersion: uint64(item.AssetVersion),
AssetID: item.AssetID,
ValidatedAssetID: validated_asset_id,
ValidatedAssetVersion: validated_asset_version,
TargetAssetID: item.TargetAssetID,
StatusID: mapfixes.MapfixStatus(item.StatusID),
}, nil
}
func (svc *Mapfixes) GetList(ctx context.Context, request *mapfixes.MapfixIdList) (*mapfixes.MapfixList, error) {
items, err := svc.inner.GetMapfixList(ctx, request.ID)
if err != nil {
return nil, err
}
resp := mapfixes.MapfixList{}
resp.Mapfixes = make([]*mapfixes.MapfixResponse, len(items))
for i, item := range items {
var validated_asset_id *uint64
if item.ValidatedAssetID != 0 {
validated_asset_id = &item.ValidatedAssetID
}
var validated_asset_version *uint64
if item.ValidatedAssetVersion != 0 {
validated_asset_version = &item.ValidatedAssetVersion
}
resp.Mapfixes[i] = &mapfixes.MapfixResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
AssetVersion: uint64(item.AssetVersion),
AssetID: item.AssetID,
ValidatedAssetID: validated_asset_id,
ValidatedAssetVersion: validated_asset_version,
TargetAssetID: item.TargetAssetID,
StatusID: mapfixes.MapfixStatus(item.StatusID),
}
}
return &resp, nil
}
func (svc *Mapfixes) List(ctx context.Context, request *mapfixes.ListRequest) (*mapfixes.MapfixList, error) {
if request.Page == nil {
return nil, PageError
}
filter := service.NewMapfixFilter()
if request.Filter != nil {
if request.Filter.DisplayName != nil {
filter.SetDisplayName(*request.Filter.DisplayName)
}
if request.Filter.Creator != nil {
filter.SetCreator(*request.Filter.Creator)
}
if request.Filter.GameID != nil {
filter.SetGameID(*request.Filter.GameID)
}
if request.Filter.Submitter != nil {
filter.SetSubmitter(*request.Filter.Submitter)
}
}
items, err := svc.inner.ListMapfixes(ctx, filter, model.Page{
Number: int32(request.Page.Number),
Size: int32(request.Page.Size),
}, datastore.ListSortDateDescending)
if err != nil {
return nil, err
}
resp := mapfixes.MapfixList{}
resp.Mapfixes = make([]*mapfixes.MapfixResponse, len(items))
for i, item := range items {
var validated_asset_id *uint64
if item.ValidatedAssetID != 0 {
validated_asset_id = &item.ValidatedAssetID
}
var validated_asset_version *uint64
if item.ValidatedAssetVersion != 0 {
validated_asset_version = &item.ValidatedAssetVersion
}
resp.Mapfixes[i] = &mapfixes.MapfixResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
AssetVersion: uint64(item.AssetVersion),
AssetID: item.AssetID,
ValidatedAssetID: validated_asset_id,
ValidatedAssetVersion: validated_asset_version,
TargetAssetID: item.TargetAssetID,
StatusID: mapfixes.MapfixStatus(item.StatusID),
}
}
return &resp, nil
}

View File

@@ -195,13 +195,3 @@ func (svc *Maps) IncrementLoadCount(ctx context.Context, request *maps_extended.
}
return &maps_extended.NullResponse{}, nil
}
func (svc *Maps) GetSnfmDownloadUrl(ctx context.Context, request *maps_extended.MapId) (*maps_extended.SnfmDownloadUrl, error) {
url, err := svc.inner.GetSnfmDownloadUrl(ctx, request.ID)
if err != nil {
return nil, err
}
return &maps_extended.SnfmDownloadUrl{
Url: url,
}, nil
}

View File

@@ -1,161 +0,0 @@
package controller
import (
"context"
"git.itzana.me/strafesnet/go-grpc/submissions"
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
"git.itzana.me/strafesnet/maps-service/pkg/model"
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
type Submissions struct {
*submissions.UnimplementedSubmissionsServiceServer
inner *service.Service
}
func NewSubmissionsController(
inner *service.Service,
) Submissions {
return Submissions{
inner: inner,
}
}
func (svc *Submissions) Get(ctx context.Context, request *submissions.SubmissionId) (*submissions.SubmissionResponse, error) {
item, err := svc.inner.GetSubmission(ctx, request.ID)
if err != nil {
return nil, err
}
var validated_asset_id *uint64
if item.ValidatedAssetID != 0 {
validated_asset_id = &item.ValidatedAssetID
}
var validated_asset_version *uint64
if item.ValidatedAssetVersion != 0 {
validated_asset_version = &item.ValidatedAssetVersion
}
var uploaded_asset_id *uint64
if item.UploadedAssetID != 0 {
uploaded_asset_id = &item.UploadedAssetID
}
return &submissions.SubmissionResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
AssetVersion: uint64(item.AssetVersion),
AssetID: item.AssetID,
ValidatedAssetID: validated_asset_id,
ValidatedAssetVersion: validated_asset_version,
UploadedAssetID: uploaded_asset_id,
StatusID: submissions.SubmissionStatus(item.StatusID),
}, nil
}
func (svc *Submissions) GetList(ctx context.Context, request *submissions.SubmissionIdList) (*submissions.SubmissionList, error) {
items, err := svc.inner.GetSubmissionList(ctx, request.ID)
if err != nil {
return nil, err
}
resp := submissions.SubmissionList{}
resp.Submissions = make([]*submissions.SubmissionResponse, len(items))
for i, item := range items {
var validated_asset_id *uint64
if item.ValidatedAssetID != 0 {
validated_asset_id = &item.ValidatedAssetID
}
var validated_asset_version *uint64
if item.ValidatedAssetVersion != 0 {
validated_asset_version = &item.ValidatedAssetVersion
}
var uploaded_asset_id *uint64
if item.UploadedAssetID != 0 {
uploaded_asset_id = &item.UploadedAssetID
}
resp.Submissions[i] = &submissions.SubmissionResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
AssetVersion: uint64(item.AssetVersion),
AssetID: item.AssetID,
ValidatedAssetID: validated_asset_id,
ValidatedAssetVersion: validated_asset_version,
UploadedAssetID: uploaded_asset_id,
StatusID: submissions.SubmissionStatus(item.StatusID),
}
}
return &resp, nil
}
func (svc *Submissions) List(ctx context.Context, request *submissions.ListRequest) (*submissions.SubmissionList, error) {
if request.Page == nil {
return nil, PageError
}
filter := service.NewSubmissionFilter()
if request.Filter != nil {
if request.Filter.DisplayName != nil {
filter.SetDisplayName(*request.Filter.DisplayName)
}
if request.Filter.Creator != nil {
filter.SetCreator(*request.Filter.Creator)
}
if request.Filter.GameID != nil {
filter.SetGameID(*request.Filter.GameID)
}
if request.Filter.Submitter != nil {
filter.SetSubmitter(*request.Filter.Submitter)
}
}
items, err := svc.inner.ListSubmissions(ctx, filter, model.Page{
Number: int32(request.Page.Number),
Size: int32(request.Page.Size),
}, datastore.ListSortDateDescending)
if err != nil {
return nil, err
}
resp := submissions.SubmissionList{}
resp.Submissions = make([]*submissions.SubmissionResponse, len(items))
for i, item := range items {
var validated_asset_id *uint64
if item.ValidatedAssetID != 0 {
validated_asset_id = &item.ValidatedAssetID
}
var validated_asset_version *uint64
if item.ValidatedAssetVersion != 0 {
validated_asset_version = &item.ValidatedAssetVersion
}
var uploaded_asset_id *uint64
if item.UploadedAssetID != 0 {
uploaded_asset_id = &item.UploadedAssetID
}
resp.Submissions[i] = &submissions.SubmissionResponse{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: uint64(item.Submitter),
AssetVersion: uint64(item.AssetVersion),
AssetID: item.AssetID,
ValidatedAssetID: validated_asset_id,
ValidatedAssetVersion: validated_asset_version,
UploadedAssetID: uploaded_asset_id,
StatusID: submissions.SubmissionStatus(item.StatusID),
}
}
return &resp, nil
}

View File

@@ -47,7 +47,6 @@ type Maps interface {
Create(ctx context.Context, smap model.Map) (model.Map, error)
Update(ctx context.Context, id int64, values OptionalMap) error
Delete(ctx context.Context, id int64) error
GetAll(ctx context.Context) ([]model.Map, error)
List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.Map, error)
IncrementLoadCount(ctx context.Context, id int64) error
}

View File

@@ -23,14 +23,6 @@ func (q OptionalMap) AddNotNil(column string, value interface{}) OptionalMap {
return q
}
func (q OptionalMap) Pop(column string) (interface{}, bool) {
value, ok := q.filter[column]
if ok {
delete(q.filter, column)
}
return value, ok
}
func (q OptionalMap) Map() map[string]interface{} {
return q.filter
}

View File

@@ -74,21 +74,9 @@ func (env *Maps) Delete(ctx context.Context, id int64) error {
return nil
}
func (env *Maps) GetAll(ctx context.Context) ([]model.Map, error) {
var maps []model.Map
if err := env.db.Find(&maps).Error; err != nil {
return nil, err
}
return maps, nil
}
func (env *Maps) List(ctx context.Context, filters datastore.OptionalMap, page model.Page) ([]model.Map, error) {
var events []model.Map
tx := env.db.Model(&model.Map{})
if displayName, ok := filters.Pop("display_name"); ok {
tx = tx.Where("display_name ILIKE ?", "%"+displayName.(string)+"%")
}
if err := tx.Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&events).Error; err != nil {
if err := env.db.Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&events).Error; err != nil {
return nil, err
}

View File

@@ -18,12 +18,10 @@ const (
MapfixStatusValidating MapfixStatus = 5
MapfixStatusValidated MapfixStatus = 6
MapfixStatusUploading MapfixStatus = 7
MapfixStatusUploaded MapfixStatus = 8 // uploaded to the group, but pending release
MapfixStatusReleasing MapfixStatus = 11
// Phase: Final MapfixStatus
MapfixStatusUploaded MapfixStatus = 8 // uploaded to the group, but pending release
MapfixStatusRejected MapfixStatus = 9
MapfixStatusReleased MapfixStatus = 10
)
type Mapfix struct {

View File

@@ -65,33 +65,3 @@ type UploadMapfixRequest struct {
ModelVersion uint64
TargetAssetID uint64
}
type ReleaseSubmissionRequest struct {
// Release schedule
SubmissionID int64
ReleaseDate int64
// Model download info
ModelID uint64
ModelVersion uint64
// MapCreate
UploadedAssetID uint64
DisplayName string
Creator string
GameID uint32
Submitter uint64
}
type BatchReleaseSubmissionsRequest struct {
Submissions []ReleaseSubmissionRequest
OperationID int32
}
type ReleaseMapfixRequest struct {
MapfixID int64
ModelID uint64
ModelVersion uint64
TargetAssetID uint64
}
type SeedCombobulatorRequest struct {
AssetID uint64
}

View File

@@ -17,7 +17,7 @@ type ScriptPolicy struct {
// Hash of the source code that leads to this policy.
// If this is a replacement mapping, the original source may not be pointed to by any policy.
// The original source should still exist in the scripts table, which can be located by the same hash.
FromScriptHash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
FromScriptHash int64 // postgres does not support unsigned integers, so we have to pretend
// The ID of the replacement source (ScriptPolicyReplace)
// or verbatim source (ScriptPolicyAllowed)
// or 0 (other)

View File

@@ -26,7 +26,7 @@ func HashParse(hash string) (uint64, error){
type Script struct {
ID int64 `gorm:"primaryKey"`
Name string
Hash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend
Hash int64 // postgres does not support unsigned integers, so we have to pretend
Source string
ResourceType ResourceType // is this a submission or is it a mapfix
ResourceID int64 // which submission / mapfix did this script first appear in

View File

@@ -1,48 +0,0 @@
package dto
import (
"time"
"git.itzana.me/strafesnet/go-grpc/maps_extended"
)
type MapFilter struct {
GameID *uint32 `json:"game_id" form:"game_id"`
} // @name MapFilter
type Map struct {
ID int64 `json:"id"`
DisplayName string `json:"display_name"`
Creator string `json:"creator"`
GameID uint32 `json:"game_id"`
Date time.Time `json:"date"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Submitter uint64 `json:"submitter"`
Thumbnail uint64 `json:"thumbnail"`
AssetVersion uint64 `json:"asset_version"`
LoadCount uint32 `json:"load_count"`
Modes uint32 `json:"modes"`
} // @name Map
// FromGRPC converts a maps.MapResponse protobuf message to a Map domain object
func (m *Map) FromGRPC(resp *maps_extended.MapResponse) *Map {
if resp == nil {
return nil
}
m.ID = resp.ID
m.DisplayName = resp.DisplayName
m.Creator = resp.Creator
m.Date = time.Unix(resp.Date, 0)
m.GameID = resp.GameID
m.CreatedAt = time.Unix(resp.CreatedAt, 0)
m.UpdatedAt = time.Unix(resp.UpdatedAt, 0)
m.Submitter = resp.Submitter
m.Thumbnail = resp.Thumbnail
m.AssetVersion = resp.AssetVersion
m.LoadCount = resp.LoadCount
m.Modes = resp.Modes
return m
}

View File

@@ -1,52 +0,0 @@
package dto
// @Description Generic response
type Response[T any] struct {
// Data contains the actual response payload
Data T `json:"data"`
} // @name Response
type PagedTotalResponse[T any] struct {
// Data contains the actual response payload
Data []T `json:"data"`
// Pagination contains information about paging
Pagination PaginationWithTotal `json:"pagination"`
} // @name PagedTotalResponse
// PaginationWithTotal holds information about the current page, total items, etc.
type PaginationWithTotal struct {
// Current page number
Page int `json:"page"`
// Number of items per page
PageSize int `json:"page_size"`
// Total number of items across all pages
TotalItems int `json:"total_items"`
// Total number of pages
TotalPages int `json:"total_pages"`
} // @name PaginationWithTotal
type PagedResponse[T any] struct {
// Data contains the actual response payload
Data []T `json:"data"`
// Pagination contains information about paging
Pagination Pagination `json:"pagination"`
} // @name PagedResponse
// Pagination holds information about the current page.
type Pagination struct {
// Current page number
Page int `json:"page"`
// Number of items per page
PageSize int `json:"page_size"`
} // @name Pagination
// Error holds error responses
type Error struct {
Error string `json:"error"`
} // @name Error

View File

@@ -1,98 +0,0 @@
package handlers
import (
"fmt"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"strconv"
)
const (
ErrMsgDataClient = "data client is required"
)
// Handler is a base handler that provides common functionality for all HTTP handlers.
type Handler struct {
mapsClient *grpc.ClientConn
}
// HandlerOption defines a functional option for configuring a Handler
type HandlerOption func(*Handler)
// WithMapsClient sets the data client for the Handler
func WithMapsClient(mapsClient *grpc.ClientConn) HandlerOption {
return func(h *Handler) {
h.mapsClient = mapsClient
}
}
// NewHandler creates a new Handler with the provided options.
// It requires both a datastore and an authentication service to function properly.
func NewHandler(options ...HandlerOption) (*Handler, error) {
handler := &Handler{}
// Apply all provided options
for _, option := range options {
option(handler)
}
// Validate required dependencies
if err := handler.validateDependencies(); err != nil {
return nil, err
}
return handler, nil
}
// validateDependencies ensures all required dependencies are properly set
func (h *Handler) validateDependencies() error {
if h.mapsClient == nil {
return fmt.Errorf(ErrMsgDataClient)
}
return nil
}
// validateRange ensures a value is within the specified range, returning defaultValue if outside
func validateRange(value, min, max, defaultValue int) int {
if value < min {
return defaultValue
}
if value > max {
return max
}
return value
}
// validateMin ensures a value is at least the minimum, returning defaultValue if below
func validateMin(value, min, defaultValue int) int {
if value < min {
return defaultValue
}
return value
}
// getPagination extracts pagination parameters from query string.
// It applies validation rules to ensure parameters are within acceptable ranges.
func getPagination(ctx *gin.Context, defaultPageSize, minPageSize, maxPageSize int) (pageSize, pageNumber int) {
// Get page size from query string, parse to integer
pageSizeStr := ctx.Query("page_size")
if pageSizeStr != "" {
pageSize, _ = strconv.Atoi(pageSizeStr)
} else {
pageSize = defaultPageSize
}
// Get page number from query string, parse to integer
pageNumberStr := ctx.Query("page_number")
if pageNumberStr != "" {
pageNumber, _ = strconv.Atoi(pageNumberStr)
} else {
pageNumber = 1 // Default to first page
}
// Apply validation rules
pageSize = validateRange(pageSize, minPageSize, maxPageSize, defaultPageSize)
pageNumber = validateMin(pageNumber, 1, 1)
return pageSize, pageNumber
}

View File

@@ -1,194 +0,0 @@
package handlers
import (
"git.itzana.me/strafesnet/go-grpc/maps_extended"
"git.itzana.me/strafesnet/maps-service/pkg/public_api/dto"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http"
"strconv"
)
// MapHandler handles HTTP requests related to maps.
type MapHandler struct {
*Handler
}
// NewMapHandler creates a new MapHandler with the provided options.
func NewMapHandler(options ...HandlerOption) (*MapHandler, error) {
baseHandler, err := NewHandler(options...)
if err != nil {
return nil, err
}
return &MapHandler{
Handler: baseHandler,
}, nil
}
// @Summary Get map by ID
// @Description Get a specific map by its ID
// @Tags maps
// @Produce json
// @Security ApiKeyAuth
// @Param id path int true "Map ID"
// @Success 200 {object} dto.Response[dto.Map]
// @Failure 404 {object} dto.Error "Map not found"
// @Failure default {object} dto.Error "General error response"
// @Router /map/{id} [get]
func (h *MapHandler) Get(ctx *gin.Context) {
// Extract map ID from path parameter
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
}
// Call the gRPC service
mapData, err := maps_extended.NewMapsServiceClient(h.mapsClient).Get(ctx, &maps_extended.MapId{
ID: mapID,
})
if err != nil {
statusCode := http.StatusInternalServerError
errorMessage := "Failed to get map"
// Check if it's a "not found" error
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 map",
)
return
}
// Convert gRPC MapResponse object to dto.Map object
var mapDto dto.Map
result := mapDto.FromGRPC(mapData)
// Return the map data
ctx.JSON(http.StatusOK, dto.Response[dto.Map]{
Data: *result,
})
}
// @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
// @Produce json
// @Security ApiKeyAuth
// @Param page_size query int false "Page size (max 100)" default(10) minimum(1) maximum(100)
// @Param page_number query int false "Page number" default(1) minimum(1)
// @Param filter query dto.MapFilter false "Map filter parameters"
// @Success 200 {object} dto.PagedResponse[dto.Map]
// @Failure default {object} dto.Error "General error response"
// @Router /map [get]
func (h *MapHandler) List(ctx *gin.Context) {
// Extract and constrain pagination parameters
query := struct {
PageSize int `form:"page_size,default=10" binding:"min=1,max=100"`
PageNumber int `form:"page_number,default=1" binding:"min=1"`
SortBy int `form:"sort_by,default=0" binding:"min=0,max=3"`
}{}
if err := ctx.ShouldBindQuery(&query); err != nil {
ctx.JSON(http.StatusBadRequest, dto.Error{
Error: err.Error(),
})
return
}
// Get list filter
var filter dto.MapFilter
if err := ctx.ShouldBindQuery(&filter); err != nil {
ctx.JSON(http.StatusBadRequest, dto.Error{
Error: err.Error(),
})
return
}
// Call the gRPC service
mapList, err := maps_extended.NewMapsServiceClient(h.mapsClient).List(ctx, &maps_extended.ListRequest{
Filter: &maps_extended.MapFilter{
GameID: filter.GameID,
},
Page: &maps_extended.Pagination{
Size: uint32(query.PageSize),
Number: uint32(query.PageNumber),
},
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, dto.Error{
Error: "Failed to list maps",
})
log.WithError(err).Error(
"Failed to list maps",
)
return
}
// Convert gRPC MapResponse objects to dto.Map objects
dtoMaps := make([]dto.Map, len(mapList.Maps))
for i, m := range mapList.Maps {
var mapDto dto.Map
dtoMaps[i] = *mapDto.FromGRPC(m)
}
// Return the paged response
ctx.JSON(http.StatusOK, dto.PagedResponse[dto.Map]{
Data: dtoMaps,
Pagination: dto.Pagination{
Page: query.PageNumber,
PageSize: query.PageSize,
},
})
}

View File

@@ -1,166 +0,0 @@
package api
import (
"context"
"errors"
"fmt"
"git.itzana.me/StrafesNET/dev-service/pkg/api/middleware"
"git.itzana.me/strafesnet/maps-service/docs"
"git.itzana.me/strafesnet/maps-service/pkg/public_api/handlers"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
"net/http"
"time"
)
// Option defines a function that configures a Router
type Option func(*RouterConfig)
// RouterConfig holds all router configuration
type RouterConfig struct {
port int
devClient *grpc.ClientConn
mapsClient *grpc.ClientConn
context *cli.Context
shutdownTimeout time.Duration
}
// WithPort sets the port for the server£
func WithPort(port int) Option {
return func(cfg *RouterConfig) {
cfg.port = port
}
}
// WithContext sets the context for the server
func WithContext(ctx *cli.Context) Option {
return func(cfg *RouterConfig) {
cfg.context = ctx
}
}
// WithDevClient sets the dev gRPC client
func WithDevClient(conn *grpc.ClientConn) Option {
return func(cfg *RouterConfig) {
cfg.devClient = conn
}
}
// WithMapsClient sets the data gRPC client
func WithMapsClient(conn *grpc.ClientConn) Option {
return func(cfg *RouterConfig) {
cfg.mapsClient = conn
}
}
// WithShutdownTimeout sets the graceful shutdown timeout
func WithShutdownTimeout(timeout time.Duration) Option {
return func(cfg *RouterConfig) {
cfg.shutdownTimeout = timeout
}
}
func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
r := gin.Default()
r.ForwardedByClientIP = true
r.Use(gin.Logger())
r.Use(gin.Recovery())
handlerOptions := []handlers.HandlerOption{
handlers.WithMapsClient(cfg.mapsClient),
}
// Maps handler
mapsHandler, err := handlers.NewMapHandler(handlerOptions...)
if err != nil {
return nil, err
}
docs.SwaggerInfo.BasePath = "/public-api/v1"
public_api := r.Group("/public-api")
{
v1 := public_api.Group("/v1")
{
// Auth middleware
v1.Use(middleware.ValidateRequest("Maps", "Read", cfg.devClient))
// Maps
v1.GET("/map", mapsHandler.List)
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) {
ctx.Redirect(http.StatusPermanentRedirect, "/public-api/docs/index.html")
})
}
return r, nil
}
// NewRouter creates a new router with the given options
func NewRouter(options ...Option) error {
// Default configuration
cfg := &RouterConfig{
port: 8080, // Default port
context: nil,
shutdownTimeout: 5 * time.Second,
}
// Apply options
for _, option := range options {
option(cfg)
}
// Validate configuration
if cfg.context == nil {
return errors.New("context is required")
}
if cfg.devClient == nil {
return errors.New("dev client is required")
}
routes, err := setupRoutes(cfg)
if err != nil {
return err
}
log.Info("Starting server")
return runServer(cfg.context.Context, fmt.Sprint(":", cfg.port), routes, cfg.shutdownTimeout)
}
func runServer(ctx context.Context, addr string, r *gin.Engine, shutdownTimeout time.Duration) error {
srv := &http.Server{
Addr: addr,
Handler: r,
}
// Run the server in a separate goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.WithError(err).Fatal("web server exit")
}
}()
// Wait for a shutdown signal
<-ctx.Done()
// Shutdown server gracefully
ctxShutdown, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
return srv.Shutdown(ctxShutdown)
}

View File

@@ -1,160 +0,0 @@
package roblox
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// ThumbnailSize represents valid Roblox thumbnail sizes
type ThumbnailSize string
const (
Size150x150 ThumbnailSize = "150x150"
Size420x420 ThumbnailSize = "420x420"
Size768x432 ThumbnailSize = "768x432"
)
// ThumbnailFormat represents the image format
type ThumbnailFormat string
const (
FormatPng ThumbnailFormat = "Png"
FormatJpeg ThumbnailFormat = "Jpeg"
)
// ThumbnailRequest represents a single thumbnail request
type ThumbnailRequest struct {
RequestID string `json:"requestId,omitempty"`
Type string `json:"type"`
TargetID uint64 `json:"targetId"`
Size string `json:"size,omitempty"`
Format string `json:"format,omitempty"`
}
// ThumbnailData represents a single thumbnail response
type ThumbnailData struct {
TargetID uint64 `json:"targetId"`
State string `json:"state"` // "Completed", "Error", "Pending"
ImageURL string `json:"imageUrl"`
}
// BatchThumbnailsResponse represents the API response
type BatchThumbnailsResponse struct {
Data []ThumbnailData `json:"data"`
}
// GetAssetThumbnails fetches thumbnails for multiple assets in a single batch request
// Roblox allows up to 100 assets per batch
func (c *Client) GetAssetThumbnails(assetIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
if len(assetIDs) == 0 {
return []ThumbnailData{}, nil
}
if len(assetIDs) > 100 {
return nil, GetError("batch size cannot exceed 100 assets")
}
// Build request payload - the API expects an array directly, not wrapped in an object
requests := make([]ThumbnailRequest, len(assetIDs))
for i, assetID := range assetIDs {
requests[i] = ThumbnailRequest{
Type: "Asset",
TargetID: assetID,
Size: string(size),
Format: string(format),
}
}
jsonData, err := json.Marshal(requests)
if err != nil {
return nil, GetError("JSONMarshalError: " + err.Error())
}
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
if err != nil {
return nil, GetError("RequestCreationError: " + err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, GetError("RequestError: " + err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, GetError("ReadBodyError: " + err.Error())
}
var response BatchThumbnailsResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, GetError("JSONUnmarshalError: " + err.Error())
}
return response.Data, nil
}
// GetUserAvatarThumbnails fetches avatar thumbnails for multiple users in a single batch request
func (c *Client) GetUserAvatarThumbnails(userIDs []uint64, size ThumbnailSize, format ThumbnailFormat) ([]ThumbnailData, error) {
if len(userIDs) == 0 {
return []ThumbnailData{}, nil
}
if len(userIDs) > 100 {
return nil, GetError("batch size cannot exceed 100 users")
}
// Build request payload - the API expects an array directly, not wrapped in an object
requests := make([]ThumbnailRequest, len(userIDs))
for i, userID := range userIDs {
requests[i] = ThumbnailRequest{
Type: "AvatarHeadShot",
TargetID: userID,
Size: string(size),
Format: string(format),
}
}
jsonData, err := json.Marshal(requests)
if err != nil {
return nil, GetError("JSONMarshalError: " + err.Error())
}
req, err := http.NewRequest("POST", "https://thumbnails.roblox.com/v1/batch", bytes.NewBuffer(jsonData))
if err != nil {
return nil, GetError("RequestCreationError: " + err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, GetError("RequestError: " + err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, GetError("ReadBodyError: " + err.Error())
}
var response BatchThumbnailsResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, GetError("JSONUnmarshalError: " + err.Error())
}
return response.Data, nil
}

View File

@@ -1,72 +0,0 @@
package roblox
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// UserData represents a single user's information
type UserData struct {
ID uint64 `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
}
// BatchUsersResponse represents the API response for batch user requests
type BatchUsersResponse struct {
Data []UserData `json:"data"`
}
// GetUsernames fetches usernames for multiple users in a single batch request
// Roblox allows up to 100 users per batch
func (c *Client) GetUsernames(userIDs []uint64) ([]UserData, error) {
if len(userIDs) == 0 {
return []UserData{}, nil
}
if len(userIDs) > 100 {
return nil, GetError("batch size cannot exceed 100 users")
}
// Build request payload
payload := map[string][]uint64{
"userIds": userIDs,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, GetError("JSONMarshalError: " + err.Error())
}
req, err := http.NewRequest("POST", "https://users.roblox.com/v1/users", bytes.NewBuffer(jsonData))
if err != nil {
return nil, GetError("RequestCreationError: " + err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, GetError("RequestError: " + err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, GetError(fmt.Sprintf("ResponseError: status code %d, body: %s", resp.StatusCode, string(body)))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, GetError("ReadBodyError: " + err.Error())
}
var response BatchUsersResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, GetError("JSONUnmarshalError: " + err.Error())
}
return response.Data, nil
}

View File

@@ -103,10 +103,6 @@ func (svc *Service) GetMapfix(ctx context.Context, id int64) (model.Mapfix, erro
return svc.db.Mapfixes().Get(ctx, id)
}
func (svc *Service) GetMapfixList(ctx context.Context, ids []int64) ([]model.Mapfix, error) {
return svc.db.Mapfixes().GetList(ctx, ids)
}
func (svc *Service) UpdateMapfix(ctx context.Context, id int64, pmap MapfixUpdate) error {
return svc.db.Mapfixes().Update(ctx, id, datastore.OptionalMap(pmap))
}

View File

@@ -27,7 +27,7 @@ func (update MapUpdate) SetGameID(game_id uint32) {
datastore.OptionalMap(update).Add("game_id", game_id)
}
func (update MapUpdate) SetDate(date int64) {
datastore.OptionalMap(update).Add("date", time.Unix(date, 0))
datastore.OptionalMap(update).Add("date", date)
}
func (update MapUpdate) SetSubmitter(submitter uint64) {
datastore.OptionalMap(update).Add("submitter", submitter)
@@ -47,12 +47,16 @@ func (update MapUpdate) GetDisplayName() (string, bool) {
value, ok := datastore.OptionalMap(update).Map()["display_name"].(string)
return value, ok
}
func (update MapUpdate) GetCreator() (string, bool) {
value, ok := datastore.OptionalMap(update).Map()["creator"].(string)
return value, ok
}
func (update MapUpdate) GetGameID() (uint32, bool) {
value, ok := datastore.OptionalMap(update).Map()["game_id"].(uint32)
return value, ok
}
func (update MapUpdate) GetThumbnail() (uint64, bool) {
value, ok := datastore.OptionalMap(update).Map()["thumbnail"].(uint64)
func (update MapUpdate) GetDate() (int64, bool) {
value, ok := datastore.OptionalMap(update).Map()["date"].(int64)
return value, ok
}
@@ -77,6 +81,35 @@ func (update MapFilter) SetSubmitter(submitter uint64) {
datastore.OptionalMap(update).Add("submitter", submitter)
}
func (svc *Service) TEMP_DoMapsMigration(ctx context.Context) (error) {
// get all maps
maps, err := svc.maps.List(ctx, &maps.ListRequest{})
if err != nil {
return err
}
// create all maps
for _, item := range maps.Maps {
migrated := model.Map{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: uint32(item.GameID),
Date: time.Unix(item.Date, 0),
// CreatedAt: time.Time{},
// UpdatedAt: time.Time{},
// Submitter: 0,
// Thumbnail: 0,
// AssetVersion: 0,
// LoadCount: 0,
// Modes: 0,
}
_, err := svc.db.Maps().Create(ctx, migrated)
if err != nil {
return err
}
}
return nil
}
func (svc *Service) CreateMap(ctx context.Context, item model.Map) (int64, error) {
// 2 jobs:
// create map on maps-service
@@ -85,12 +118,14 @@ func (svc *Service) CreateMap(ctx context.Context, item model.Map) (int64, error
return 0, err
}
// create map on data-service
date := item.Date.Unix()
game_id := int32(item.GameID)
_, err = svc.maps.Create(ctx, &maps.MapRequest{
ID: item.ID,
DisplayName: &item.DisplayName,
Creator: &item.Creator,
GameID: &game_id,
Thumbnail: &item.Thumbnail,
Date: &date,
})
if err != nil {
return 0, err
@@ -99,10 +134,6 @@ func (svc *Service) CreateMap(ctx context.Context, item model.Map) (int64, error
return map_item.ID, nil
}
func (svc *Service) GetAllMaps(ctx context.Context) ([]model.Map, error) {
return svc.db.Maps().GetAll(ctx)
}
func (svc *Service) ListMaps(ctx context.Context, filter MapFilter, page model.Page) ([]model.Map, error) {
return svc.db.Maps().List(ctx, datastore.OptionalMap(filter), page)
}
@@ -135,12 +166,15 @@ func (svc *Service) UpdateMap(ctx context.Context, id int64, pmap MapUpdate) err
if display_name, ok := pmap.GetDisplayName(); ok {
update.DisplayName = &display_name
}
if creator, ok := pmap.GetCreator(); ok {
update.Creator = &creator
}
if game_id, ok := pmap.GetGameID(); ok {
game_id_int32 := int32(game_id)
update.GameID = &game_id_int32
}
if thumbnail, ok := pmap.GetThumbnail(); ok {
update.Thumbnail = &thumbnail
if date, ok := pmap.GetDate(); ok {
update.Date = &date
}
_, err = svc.maps.Update(ctx, &update)
if err != nil {

View File

@@ -112,29 +112,3 @@ func (svc *Service) NatsValidateMapfix(
return nil
}
func (svc *Service) NatsReleaseMapfix(
MapfixID int64,
ModelID uint64,
ModelVersion uint64,
TargetAssetID uint64,
) error {
release_fix_request := model.ReleaseMapfixRequest{
MapfixID: MapfixID,
ModelID: ModelID,
ModelVersion: ModelVersion,
TargetAssetID: TargetAssetID,
}
j, err := json.Marshal(release_fix_request)
if err != nil {
return err
}
_, err = svc.nats.Publish("maptest.mapfixes.release", []byte(j))
if err != nil {
return err
}
return nil
}

View File

@@ -1,21 +0,0 @@
package service
import (
"encoding/json"
"git.itzana.me/strafesnet/maps-service/pkg/model"
)
func (svc *Service) NatsSeedCombobulator(assetID uint64) error {
request := model.SeedCombobulatorRequest{
AssetID: assetID,
}
j, err := json.Marshal(request)
if err != nil {
return err
}
_, err = svc.nats.Publish("maptest.combobulator.seed", j)
return err
}

View File

@@ -88,28 +88,6 @@ func (svc *Service) NatsUploadSubmission(
return nil
}
func (svc *Service) NatsBatchReleaseSubmissions(
Submissions []model.ReleaseSubmissionRequest,
operation int32,
) error {
release_new_request := model.BatchReleaseSubmissionsRequest{
Submissions: Submissions,
OperationID: operation,
}
j, err := json.Marshal(release_new_request)
if err != nil {
return err
}
_, err = svc.nats.Publish("maptest.submissions.batchrelease", []byte(j))
if err != nil {
return err
}
return nil
}
func (svc *Service) NatsValidateSubmission(
SubmissionID int64,
ModelID uint64,

View File

@@ -1,27 +1,17 @@
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"
)
type Service struct {
db datastore.Datastore
nats nats.JetStreamContext
maps maps.MapsServiceClient
users users.UsersServiceClient
thumbnailService *ThumbnailService
s3Presign *s3.PresignClient
s3Bucket string
db datastore.Datastore
nats nats.JetStreamContext
maps maps.MapsServiceClient
users users.UsersServiceClient
}
func NewService(
@@ -29,60 +19,11 @@ func NewService(
nats nats.JetStreamContext,
maps maps.MapsServiceClient,
users users.UsersServiceClient,
robloxClient *roblox.Client,
redisClient *redis.Client,
s3Client *s3.Client,
s3Bucket string,
) Service {
return Service{
db: db,
nats: nats,
maps: maps,
users: users,
thumbnailService: NewThumbnailService(robloxClient, redisClient),
s3Presign: s3.NewPresignClient(s3Client),
s3Bucket: s3Bucket,
db: db,
nats: nats,
maps: maps,
users: users,
}
}
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)
}
// GetUserAvatarThumbnails proxies to the thumbnail service
func (s *Service) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
return s.thumbnailService.GetUserAvatarThumbnails(ctx, userIDs, size)
}
// GetSingleAssetThumbnail proxies to the thumbnail service
func (s *Service) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
return s.thumbnailService.GetSingleAssetThumbnail(ctx, assetID, size)
}
// GetSingleUserAvatarThumbnail proxies to the thumbnail service
func (s *Service) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
return s.thumbnailService.GetSingleUserAvatarThumbnail(ctx, userID, size)
}
// GetUsernames proxies to the thumbnail service
func (s *Service) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
return s.thumbnailService.GetUsernames(ctx, userIDs)
}
// GetSingleUsername proxies to the thumbnail service
func (s *Service) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
return s.thumbnailService.GetSingleUsername(ctx, userID)
}

View File

@@ -1,218 +0,0 @@
package service
import (
"context"
"encoding/json"
"fmt"
"time"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
"github.com/redis/go-redis/v9"
)
type ThumbnailService struct {
robloxClient *roblox.Client
redisClient *redis.Client
cacheTTL time.Duration
}
func NewThumbnailService(robloxClient *roblox.Client, redisClient *redis.Client) *ThumbnailService {
return &ThumbnailService{
robloxClient: robloxClient,
redisClient: redisClient,
cacheTTL: 24 * time.Hour, // Cache thumbnails for 24 hours
}
}
// CachedThumbnail represents a cached thumbnail entry
type CachedThumbnail struct {
ImageURL string `json:"imageUrl"`
State string `json:"state"`
CachedAt time.Time `json:"cachedAt"`
}
// GetAssetThumbnails fetches thumbnails with Redis caching and batching
func (s *ThumbnailService) GetAssetThumbnails(ctx context.Context, assetIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
if len(assetIDs) == 0 {
return map[uint64]string{}, nil
}
result := make(map[uint64]string)
var missingIDs []uint64
// Try to get from cache first
for _, assetID := range assetIDs {
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", assetID, size)
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
if err == redis.Nil {
// Cache miss
missingIDs = append(missingIDs, assetID)
} else if err != nil {
// Redis error - treat as cache miss
missingIDs = append(missingIDs, assetID)
} else {
// Cache hit
var thumbnail CachedThumbnail
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
result[assetID] = thumbnail.ImageURL
} else {
missingIDs = append(missingIDs, assetID)
}
}
}
// If all were cached, return early
if len(missingIDs) == 0 {
return result, nil
}
// Batch fetch missing thumbnails from Roblox API
// Split into batches of 100 (Roblox API limit)
for i := 0; i < len(missingIDs); i += 100 {
end := i + 100
if end > len(missingIDs) {
end = len(missingIDs)
}
batch := missingIDs[i:end]
thumbnails, err := s.robloxClient.GetAssetThumbnails(batch, size, roblox.FormatPng)
if err != nil {
return nil, fmt.Errorf("failed to fetch thumbnails: %w", err)
}
// Process results and cache them
for _, thumb := range thumbnails {
cached := CachedThumbnail{
ImageURL: thumb.ImageURL,
State: thumb.State,
CachedAt: time.Now(),
}
if thumb.State == "Completed" && thumb.ImageURL != "" {
result[thumb.TargetID] = thumb.ImageURL
}
// Cache the result (even if incomplete, to avoid repeated API calls)
cacheKey := fmt.Sprintf("thumbnail:asset:%d:%s", thumb.TargetID, size)
cachedJSON, _ := json.Marshal(cached)
// Use shorter TTL for incomplete thumbnails
ttl := s.cacheTTL
if thumb.State != "Completed" {
ttl = 5 * time.Minute
}
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
}
}
return result, nil
}
// GetUserAvatarThumbnails fetches user avatar thumbnails with Redis caching and batching
func (s *ThumbnailService) GetUserAvatarThumbnails(ctx context.Context, userIDs []uint64, size roblox.ThumbnailSize) (map[uint64]string, error) {
if len(userIDs) == 0 {
return map[uint64]string{}, nil
}
result := make(map[uint64]string)
var missingIDs []uint64
// Try to get from cache first
for _, userID := range userIDs {
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", userID, size)
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
if err == redis.Nil {
// Cache miss
missingIDs = append(missingIDs, userID)
} else if err != nil {
// Redis error - treat as cache miss
missingIDs = append(missingIDs, userID)
} else {
// Cache hit
var thumbnail CachedThumbnail
if err := json.Unmarshal([]byte(cached), &thumbnail); err == nil && thumbnail.State == "Completed" {
result[userID] = thumbnail.ImageURL
} else {
missingIDs = append(missingIDs, userID)
}
}
}
// If all were cached, return early
if len(missingIDs) == 0 {
return result, nil
}
// Batch fetch missing thumbnails from Roblox API
// Split into batches of 100 (Roblox API limit)
for i := 0; i < len(missingIDs); i += 100 {
end := i + 100
if end > len(missingIDs) {
end = len(missingIDs)
}
batch := missingIDs[i:end]
thumbnails, err := s.robloxClient.GetUserAvatarThumbnails(batch, size, roblox.FormatPng)
if err != nil {
return nil, fmt.Errorf("failed to fetch user thumbnails: %w", err)
}
// Process results and cache them
for _, thumb := range thumbnails {
cached := CachedThumbnail{
ImageURL: thumb.ImageURL,
State: thumb.State,
CachedAt: time.Now(),
}
if thumb.State == "Completed" && thumb.ImageURL != "" {
result[thumb.TargetID] = thumb.ImageURL
}
// Cache the result
cacheKey := fmt.Sprintf("thumbnail:user:%d:%s", thumb.TargetID, size)
cachedJSON, _ := json.Marshal(cached)
// Use shorter TTL for incomplete thumbnails
ttl := s.cacheTTL
if thumb.State != "Completed" {
ttl = 5 * time.Minute
}
s.redisClient.Set(ctx, cacheKey, cachedJSON, ttl)
}
}
return result, nil
}
// GetSingleAssetThumbnail is a convenience method for fetching a single asset thumbnail
func (s *ThumbnailService) GetSingleAssetThumbnail(ctx context.Context, assetID uint64, size roblox.ThumbnailSize) (string, error) {
results, err := s.GetAssetThumbnails(ctx, []uint64{assetID}, size)
if err != nil {
return "", err
}
if url, ok := results[assetID]; ok {
return url, nil
}
return "", fmt.Errorf("thumbnail not available for asset %d", assetID)
}
// GetSingleUserAvatarThumbnail is a convenience method for fetching a single user avatar thumbnail
func (s *ThumbnailService) GetSingleUserAvatarThumbnail(ctx context.Context, userID uint64, size roblox.ThumbnailSize) (string, error) {
results, err := s.GetUserAvatarThumbnails(ctx, []uint64{userID}, size)
if err != nil {
return "", err
}
if url, ok := results[userID]; ok {
return url, nil
}
return "", fmt.Errorf("thumbnail not available for user %d", userID)
}

View File

@@ -1,108 +0,0 @@
package service
import (
"context"
"encoding/json"
"fmt"
"time"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
"github.com/redis/go-redis/v9"
)
// CachedUser represents a cached user entry
type CachedUser struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
CachedAt time.Time `json:"cachedAt"`
}
// GetUsernames fetches usernames with Redis caching and batching
func (s *ThumbnailService) GetUsernames(ctx context.Context, userIDs []uint64) (map[uint64]string, error) {
if len(userIDs) == 0 {
return map[uint64]string{}, nil
}
result := make(map[uint64]string)
var missingIDs []uint64
// Try to get from cache first
for _, userID := range userIDs {
cacheKey := fmt.Sprintf("user:name:%d", userID)
cached, err := s.redisClient.Get(ctx, cacheKey).Result()
if err == redis.Nil {
// Cache miss
missingIDs = append(missingIDs, userID)
} else if err != nil {
// Redis error - treat as cache miss
missingIDs = append(missingIDs, userID)
} else {
// Cache hit
var user CachedUser
if err := json.Unmarshal([]byte(cached), &user); err == nil && user.Name != "" {
result[userID] = user.Name
} else {
missingIDs = append(missingIDs, userID)
}
}
}
// If all were cached, return early
if len(missingIDs) == 0 {
return result, nil
}
// Batch fetch missing usernames from Roblox API
// Split into batches of 100 (Roblox API limit)
for i := 0; i < len(missingIDs); i += 100 {
end := i + 100
if end > len(missingIDs) {
end = len(missingIDs)
}
batch := missingIDs[i:end]
var users []roblox.UserData
var err error
users, err = s.robloxClient.GetUsernames(batch)
if err != nil {
return nil, fmt.Errorf("failed to fetch usernames: %w", err)
}
// Process results and cache them
for _, user := range users {
cached := CachedUser{
Name: user.Name,
DisplayName: user.DisplayName,
CachedAt: time.Now(),
}
if user.Name != "" {
result[user.ID] = user.Name
}
// Cache the result
cacheKey := fmt.Sprintf("user:name:%d", user.ID)
cachedJSON, _ := json.Marshal(cached)
// Cache usernames for a long time (7 days) since they rarely change
s.redisClient.Set(ctx, cacheKey, cachedJSON, 7*24*time.Hour)
}
}
return result, nil
}
// GetSingleUsername is a convenience method for fetching a single username
func (s *ThumbnailService) GetSingleUsername(ctx context.Context, userID uint64) (string, error) {
results, err := s.GetUsernames(ctx, []uint64{userID})
if err != nil {
return "", err
}
if name, ok := results[userID]; ok {
return name, nil
}
return "", fmt.Errorf("username not available for user %d", userID)
}

View File

@@ -26,8 +26,6 @@ func NewMapfixesController(
var(
// prevent two mapfixes with same asset id
ActiveMapfixStatuses = []model.MapfixStatus{
model.MapfixStatusReleasing,
model.MapfixStatusUploaded,
model.MapfixStatusUploading,
model.MapfixStatusValidated,
model.MapfixStatusValidating,
@@ -186,7 +184,7 @@ func (svc *Mapfixes) SetStatusValidated(ctx context.Context, params *validator.M
// (Internal endpoint) Role Validator changes status from Validating -> Accepted.
//
// POST /mapfixes/{MapfixID}/status/validator-failed
func (svc *Mapfixes) SetStatusNotValidated(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) {
func (svc *Mapfixes) SetStatusFailed(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) {
MapfixID := int64(params.ID)
// transaction
target_status := model.MapfixStatusAcceptedUnvalidated
@@ -255,117 +253,6 @@ func (svc *Mapfixes) SetStatusUploaded(ctx context.Context, params *validator.Ma
return &validator.NullResponse{}, nil
}
func (svc *Mapfixes) SetStatusNotUploaded(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) {
MapfixID := int64(params.ID)
// transaction
target_status := model.MapfixStatusValidated
update := service.NewMapfixUpdate()
update.SetStatusID(target_status)
allow_statuses := []model.MapfixStatus{model.MapfixStatusUploading}
err := svc.inner.UpdateMapfixIfStatus(ctx, MapfixID, allow_statuses, update)
if err != nil {
return nil, err
}
// push an action audit event
event_data := model.AuditEventDataAction{
TargetStatus: uint32(target_status),
}
err = svc.inner.CreateAuditEventAction(
ctx,
model.ValidatorUserID,
model.Resource{
ID: MapfixID,
Type: model.ResourceMapfix,
},
event_data,
)
if err != nil {
return nil, err
}
return &validator.NullResponse{}, nil
}
// ActionMapfixReleased implements actionMapfixReleased operation.
//
// (Internal endpoint) Role Validator changes status from Releasing -> Released.
//
// POST /mapfixes/{MapfixID}/status/validator-released
func (svc *Mapfixes) SetStatusReleased(ctx context.Context, params *validator.MapfixReleaseRequest) (*validator.NullResponse, error) {
MapfixID := int64(params.MapfixID)
// transaction
target_status := model.MapfixStatusReleased
update := service.NewMapfixUpdate()
update.SetStatusID(target_status)
allow_statuses := []model.MapfixStatus{model.MapfixStatusReleasing}
err := svc.inner.UpdateMapfixIfStatus(ctx, MapfixID, allow_statuses, update)
if err != nil {
return nil, err
}
event_data := model.AuditEventDataAction{
TargetStatus: uint32(target_status),
}
err = svc.inner.CreateAuditEventAction(
ctx,
model.ValidatorUserID,
model.Resource{
ID: MapfixID,
Type: model.ResourceMapfix,
},
event_data,
)
if err != nil {
return nil, err
}
// metadata maintenance
map_update := service.NewMapUpdate()
map_update.SetAssetVersion(params.AssetVersion)
map_update.SetModes(params.Modes)
err = svc.inner.UpdateMap(ctx, int64(params.TargetAssetID), map_update)
if err != nil {
return nil, err
}
return &validator.NullResponse{}, nil
}
func (svc *Mapfixes) SetStatusNotReleased(ctx context.Context, params *validator.MapfixID) (*validator.NullResponse, error) {
MapfixID := int64(params.ID)
// transaction
target_status := model.MapfixStatusUploaded
update := service.NewMapfixUpdate()
update.SetStatusID(target_status)
allow_statuses := []model.MapfixStatus{model.MapfixStatusReleasing}
err := svc.inner.UpdateMapfixIfStatus(ctx, MapfixID, allow_statuses, update)
if err != nil {
return nil, err
}
// push an action audit event
event_data := model.AuditEventDataAction{
TargetStatus: uint32(target_status),
}
err = svc.inner.CreateAuditEventAction(
ctx,
model.ValidatorUserID,
model.Resource{
ID: MapfixID,
Type: model.ResourceMapfix,
},
event_data,
)
if err != nil {
return nil, err
}
return &validator.NullResponse{}, nil
}
// CreateMapfixAuditError implements createMapfixAuditError operation.
//

View File

@@ -19,18 +19,6 @@ func NewOperationsController(
}
}
func (svc *Operations) Success(ctx context.Context, params *validator.OperationSuccessRequest) (*validator.NullResponse, error) {
success_params := service.NewOperationCompleteParams(
params.Path,
)
err := svc.inner.CompleteOperation(ctx, int32(params.OperationID), success_params)
if err != nil {
return nil, err
}
return &validator.NullResponse{}, nil
}
// ActionOperationFailed implements actionOperationFailed operation.
//
// Fail the specified OperationID with a StatusMessage.

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"time"
"git.itzana.me/strafesnet/go-grpc/validator"
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
@@ -25,7 +24,7 @@ func NewSubmissionsController(
}
var(
// prevent two submissions with same asset id
// prevent two mapfixes with same asset id
ActiveSubmissionStatuses = []model.SubmissionStatus{
model.SubmissionStatusUploading,
model.SubmissionStatusValidated,
@@ -203,7 +202,7 @@ func (svc *Submissions) SetStatusValidated(ctx context.Context, params *validato
// (Internal endpoint) Role Validator changes status from Validating -> Accepted.
//
// POST /submissions/{SubmissionID}/status/validator-failed
func (svc *Submissions) SetStatusNotValidated(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) {
func (svc *Submissions) SetStatusFailed(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) {
SubmissionID := int64(params.ID)
// transaction
target_status := model.SubmissionStatusAcceptedUnvalidated
@@ -274,68 +273,6 @@ func (svc *Submissions) SetStatusUploaded(ctx context.Context, params *validator
return &validator.NullResponse{}, nil
}
func (svc *Submissions) SetStatusNotUploaded(ctx context.Context, params *validator.SubmissionID) (*validator.NullResponse, error) {
SubmissionID := int64(params.ID)
// transaction
target_status := model.SubmissionStatusValidated
update := service.NewSubmissionUpdate()
update.SetStatusID(target_status)
allowed_statuses :=[]model.SubmissionStatus{model.SubmissionStatusUploading}
err := svc.inner.UpdateSubmissionIfStatus(ctx, SubmissionID, allowed_statuses, update)
if err != nil {
return nil, err
}
// push an action audit event
event_data := model.AuditEventDataAction{
TargetStatus: uint32(target_status),
}
err = svc.inner.CreateAuditEventAction(
ctx,
model.ValidatorUserID,
model.Resource{
ID: SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
)
if err != nil {
return nil, err
}
return &validator.NullResponse{}, nil
}
func (svc *Submissions) SetStatusReleased(ctx context.Context, params *validator.SubmissionReleaseRequest) (*validator.NullResponse, error){
// create map with go-grpc
_, err := svc.inner.CreateMap(ctx, model.Map{
ID: params.MapCreate.ID,
DisplayName: params.MapCreate.DisplayName,
Creator: params.MapCreate.Creator,
GameID: params.MapCreate.GameID,
Date: time.Unix(params.MapCreate.Date, 0),
Submitter: params.MapCreate.Submitter,
Thumbnail: 0,
AssetVersion: params.MapCreate.AssetVersion,
LoadCount: 0,
Modes: params.MapCreate.Modes,
})
if err != nil {
return nil, err
}
// update status to Released
update := service.NewSubmissionUpdate()
update.SetStatusID(model.SubmissionStatusReleased)
err = svc.inner.UpdateSubmissionIfStatus(ctx, int64(params.SubmissionID), []model.SubmissionStatus{model.SubmissionStatusUploaded}, update)
if err != nil {
return nil, err
}
return &validator.NullResponse{}, nil
}
// CreateSubmissionAuditError implements createSubmissionAuditError operation.
//
// Post an error to the audit log

View File

@@ -2,6 +2,7 @@ package web_api
import (
"context"
"errors"
"fmt"
"io"
"time"
@@ -21,8 +22,6 @@ var(
}
// limit mapfixes in the pipeline to one per target map
ActiveAcceptedMapfixStatuses = []model.MapfixStatus{
model.MapfixStatusReleasing,
model.MapfixStatusUploaded,
model.MapfixStatusUploading,
model.MapfixStatusValidated,
model.MapfixStatusValidating,
@@ -34,10 +33,10 @@ var(
)
var (
ErrCreationPhaseMapfixesLimit = fmt.Errorf("%w: Active mapfixes limited to 20", ErrPermissionDenied)
ErrActiveMapfixSameTargetAssetID = fmt.Errorf("%w: There is an active mapfix for this map already", ErrPermissionDenied)
ErrCreationPhaseMapfixesLimit = errors.New("Active mapfixes limited to 20")
ErrActiveMapfixSameTargetAssetID = errors.New("There is an active mapfix for this map already")
ErrAcceptOwnMapfix = fmt.Errorf("%w: You cannot accept your own mapfix as the submitter", ErrPermissionDenied)
ErrCreateMapfixRateLimit = fmt.Errorf("%w: You must not create more than 5 mapfixes every 10 minutes", ErrTooManyRequests)
ErrCreateMapfixRateLimit = errors.New("You must not create more than 5 mapfixes every 10 minutes")
)
// POST /mapfixes
@@ -194,9 +193,6 @@ func (svc *Service) ListMapfixes(ctx context.Context, params api.ListMapfixesPar
if asset_id, asset_id_ok := params.AssetID.Get(); asset_id_ok{
filter.SetAssetID(uint64(asset_id))
}
if asset_version, asset_version_ok := params.AssetVersion.Get(); asset_version_ok{
filter.SetAssetVersion(uint64(asset_version))
}
if target_asset_id, target_asset_id_ok := params.TargetAssetID.Get(); target_asset_id_ok{
filter.SetTargetAssetID(uint64(target_asset_id))
}
@@ -326,48 +322,6 @@ func (svc *Service) UpdateMapfixModel(ctx context.Context, params api.UpdateMapf
)
}
// UpdateMapfixDescription implements updateMapfixDescription operation.
//
// Update description (submitter only, status ChangesRequested or UnderConstruction).
//
// PATCH /mapfixes/{MapfixID}/description
func (svc *Service) UpdateMapfixDescription(ctx context.Context, req api.UpdateMapfixDescriptionReq, params api.UpdateMapfixDescriptionParams) error {
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return ErrUserInfo
}
// read mapfix
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
if err != nil {
return err
}
userId, err := userInfo.GetUserID()
if err != nil {
return err
}
// check if caller is the submitter
if userId != mapfix.Submitter {
return ErrPermissionDeniedNotSubmitter
}
// read the new description from request body
data, err := io.ReadAll(req)
if err != nil {
return err
}
newDescription := string(data)
// check if Status is ChangesRequested or UnderConstruction
update := service.NewMapfixUpdate()
update.SetDescription(newDescription)
allow_statuses := []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusUnderConstruction}
return svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
}
// ActionMapfixReject invokes actionMapfixReject operation.
//
// Role Reviewer changes status from Submitted -> Rejected.
@@ -447,12 +401,7 @@ func (svc *Service) ActionMapfixRequestChanges(ctx context.Context, params api.A
target_status := model.MapfixStatusChangesRequested
update := service.NewMapfixUpdate()
update.SetStatusID(target_status)
allow_statuses := []model.MapfixStatus{
model.MapfixStatusUploaded,
model.MapfixStatusValidated,
model.MapfixStatusAcceptedUnvalidated,
model.MapfixStatusSubmitted,
}
allow_statuses := []model.MapfixStatus{model.MapfixStatusValidated, model.MapfixStatusAcceptedUnvalidated, model.MapfixStatusSubmitted}
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
if err != nil {
return err
@@ -558,11 +507,7 @@ func (svc *Service) ActionMapfixTriggerSubmit(ctx context.Context, params api.Ac
target_status := model.MapfixStatusSubmitting
update := service.NewMapfixUpdate()
update.SetStatusID(target_status)
allow_statuses := []model.MapfixStatus{
model.MapfixStatusUnderConstruction,
model.MapfixStatusChangesRequested,
model.MapfixStatusSubmitted,
}
allow_statuses := []model.MapfixStatus{model.MapfixStatusUnderConstruction, model.MapfixStatusChangesRequested}
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
if err != nil {
return err
@@ -841,127 +786,6 @@ func (svc *Service) ActionMapfixValidated(ctx context.Context, params api.Action
)
}
// ActionMapfixTriggerRelease invokes actionMapfixTriggerRelease operation.
//
// Role MapfixUpload changes status from Uploaded -> Releasing.
//
// POST /mapfixes/{MapfixID}/status/trigger-release
func (svc *Service) ActionMapfixTriggerRelease(ctx context.Context, params api.ActionMapfixTriggerReleaseParams) error {
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return ErrUserInfo
}
has_role, err := userInfo.HasRoleMapfixUpload()
if err != nil {
return err
}
// check if caller has required role
if !has_role {
return ErrPermissionDeniedNeedRoleMapfixUpload
}
userId, err := userInfo.GetUserID()
if err != nil {
return err
}
// transaction
target_status := model.MapfixStatusReleasing
update := service.NewMapfixUpdate()
update.SetStatusID(target_status)
allow_statuses := []model.MapfixStatus{model.MapfixStatusUploaded}
mapfix, err := svc.inner.UpdateAndGetMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
if err != nil {
return err
}
// this is a map fix
err = svc.inner.NatsReleaseMapfix(
mapfix.ID,
mapfix.ValidatedAssetID,
mapfix.ValidatedAssetVersion,
mapfix.TargetAssetID,
)
if err != nil {
return err
}
event_data := model.AuditEventDataAction{
TargetStatus: uint32(target_status),
}
return svc.inner.CreateAuditEventAction(
ctx,
userId,
model.Resource{
ID: params.MapfixID,
Type: model.ResourceMapfix,
},
event_data,
)
}
// ActionMapfixUploaded invokes actionMapfixUploaded operation.
//
// Role MapfixUpload manually resets releasing softlock and changes status from Releasing -> Uploaded.
//
// POST /mapfixes/{MapfixID}/status/reset-releasing
func (svc *Service) ActionMapfixUploaded(ctx context.Context, params api.ActionMapfixUploadedParams) error {
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return ErrUserInfo
}
has_role, err := userInfo.HasRoleMapfixUpload()
if err != nil {
return err
}
// check if caller has required role
if !has_role {
return ErrPermissionDeniedNeedRoleMapfixUpload
}
userId, err := userInfo.GetUserID()
if err != nil {
return err
}
// check when mapfix was updated
mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID)
if err != nil {
return err
}
if time.Now().Before(mapfix.UpdatedAt.Add(time.Second*10)) {
// the last time the mapfix was updated must be longer than 10 seconds ago
return ErrDelayReset
}
// transaction
target_status := model.MapfixStatusUploaded
update := service.NewMapfixUpdate()
update.SetStatusID(target_status)
allow_statuses := []model.MapfixStatus{model.MapfixStatusReleasing}
err = svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update)
if err != nil {
return err
}
event_data := model.AuditEventDataAction{
TargetStatus: uint32(target_status),
}
return svc.inner.CreateAuditEventAction(
ctx,
userId,
model.Resource{
ID: params.MapfixID,
Type: model.ResourceMapfix,
},
event_data,
)
}
// ActionMapfixTriggerValidate invokes actionMapfixTriggerValidate operation.
//
// Role Reviewer triggers validation and changes status from Submitted -> Validating.

View File

@@ -9,6 +9,29 @@ import (
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
// MigrateMaps implements migrateMaps operation.
//
// Perform maps migration.
//
// POST /migrate
func (svc *Service) MigrateMaps(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
}
return svc.inner.TEMP_DoMapsMigration(ctx)
}
// ListMaps implements listMaps operation.
//
// Get list of maps.
@@ -46,13 +69,6 @@ func (svc *Service) ListMaps(ctx context.Context, params api.ListMapsParams) ([]
Creator: item.Creator,
GameID: int32(item.GameID),
Date: item.Date.Unix(),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: item.Submitter,
Thumbnail: item.Thumbnail,
AssetVersion: item.AssetVersion,
LoadCount: item.LoadCount,
Modes: item.Modes,
})
}
@@ -76,71 +92,9 @@ func (svc *Service) GetMap(ctx context.Context, params api.GetMapParams) (*api.M
Creator: mapResponse.Creator,
GameID: int32(mapResponse.GameID),
Date: mapResponse.Date.Unix(),
CreatedAt: mapResponse.CreatedAt.Unix(),
UpdatedAt: mapResponse.UpdatedAt.Unix(),
Submitter: mapResponse.Submitter,
Thumbnail: mapResponse.Thumbnail,
AssetVersion: mapResponse.AssetVersion,
LoadCount: mapResponse.LoadCount,
Modes: mapResponse.Modes,
}, 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

@@ -36,28 +36,10 @@ func (svc *Service) CreateScript(ctx context.Context, req *api.ScriptCreate) (*a
return nil, err
}
hash := int64(model.HashSource(req.Source))
// Check if a script with this hash already exists
filter := service.NewScriptFilter()
filter.SetHash(hash)
existingScripts, err := svc.inner.ListScripts(ctx, filter, model.Page{Number: 1, Size: 1})
if err != nil {
return nil, err
}
// If script with this hash exists, return existing script ID
if len(existingScripts) > 0 {
return &api.ScriptID{
ScriptID: existingScripts[0].ID,
}, nil
}
// Create new script
script, err := svc.inner.CreateScript(ctx, model.Script{
ID: 0,
Name: req.Name,
Hash: hash,
Hash: int64(model.HashSource(req.Source)),
Source: req.Source,
ResourceType: model.ResourceType(req.ResourceType),
ResourceID: req.ResourceID.Or(0),

View File

@@ -2,7 +2,7 @@ package web_api
import (
"context"
"fmt"
"errors"
"git.itzana.me/strafesnet/go-grpc/auth"
"git.itzana.me/strafesnet/maps-service/pkg/api"
@@ -11,9 +11,9 @@ import (
var (
// ErrMissingSessionID there is no session id
ErrMissingSessionID = fmt.Errorf("%w: SessionID missing", ErrUserInfo)
ErrMissingSessionID = errors.New("SessionID missing")
// ErrInvalidSession caller does not have a valid session
ErrInvalidSession = fmt.Errorf("%w: Session invalid", ErrUserInfo)
ErrInvalidSession = errors.New("Session invalid")
)
type UserInfoHandle struct {
@@ -58,7 +58,7 @@ func (usr UserInfoHandle) Validate() (bool, error) {
}
return validate.Valid, nil
}
func (usr UserInfoHandle) HasRoles(wantRoles model.Roles) (bool, error) {
func (usr UserInfoHandle) hasRoles(wantRoles model.Roles) (bool, error) {
haveroles, err := usr.GetRoles()
if err != nil {
return false, err
@@ -94,25 +94,25 @@ func (usr UserInfoHandle) GetRoles() (model.Roles, error) {
// RoleThumbnail
func (usr UserInfoHandle) HasRoleMapfixUpload() (bool, error) {
return usr.HasRoles(model.RolesMapfixUpload)
return usr.hasRoles(model.RolesMapfixUpload)
}
func (usr UserInfoHandle) HasRoleMapfixReview() (bool, error) {
return usr.HasRoles(model.RolesMapfixReview)
return usr.hasRoles(model.RolesMapfixReview)
}
func (usr UserInfoHandle) HasRoleMapDownload() (bool, error) {
return usr.HasRoles(model.RolesMapDownload)
return usr.hasRoles(model.RolesMapDownload)
}
func (usr UserInfoHandle) HasRoleSubmissionRelease() (bool, error) {
return usr.HasRoles(model.RolesSubmissionRelease)
return usr.hasRoles(model.RolesSubmissionRelease)
}
func (usr UserInfoHandle) HasRoleSubmissionUpload() (bool, error) {
return usr.HasRoles(model.RolesSubmissionUpload)
return usr.hasRoles(model.RolesSubmissionUpload)
}
func (usr UserInfoHandle) HasRoleSubmissionReview() (bool, error) {
return usr.HasRoles(model.RolesSubmissionReview)
return usr.hasRoles(model.RolesSubmissionReview)
}
func (usr UserInfoHandle) HasRoleScriptWrite() (bool, error) {
return usr.HasRoles(model.RolesScriptWrite)
return usr.hasRoles(model.RolesScriptWrite)
}
/// Not implemented
func (usr UserInfoHandle) HasRoleMaptest() (bool, error) {

View File

@@ -12,8 +12,6 @@ import (
)
var (
ErrBadRequest = errors.New("Bad request")
ErrTooManyRequests = errors.New("Too many requests")
// ErrPermissionDenied caller does not have the required role
ErrPermissionDenied = errors.New("Permission denied")
// ErrUserInfo user info is missing for some reason
@@ -28,7 +26,7 @@ var (
ErrPermissionDeniedNeedRoleMapDownload = fmt.Errorf("%w: Need Role MapDownload", ErrPermissionDenied)
ErrPermissionDeniedNeedRoleScriptWrite = fmt.Errorf("%w: Need Role ScriptWrite", ErrPermissionDenied)
ErrPermissionDeniedNeedRoleMaptest = fmt.Errorf("%w: Need Role Maptest", ErrPermissionDenied)
ErrNegativeID = fmt.Errorf("%w: A negative ID was provided", ErrBadRequest)
ErrNegativeID = errors.New("A negative ID was provided")
)
type Service struct {
@@ -51,20 +49,14 @@ func NewService(
// Used for common default response.
func (svc *Service) NewError(ctx context.Context, err error) *api.ErrorStatusCode {
status := 500
if errors.Is(err, ErrBadRequest) {
status = 400
}
if errors.Is(err, ErrUserInfo) {
status = 401
if errors.Is(err, datastore.ErrNotExist) {
status = 404
}
if errors.Is(err, ErrPermissionDenied) {
status = 403
}
if errors.Is(err, datastore.ErrNotExist) {
status = 404
}
if errors.Is(err, ErrTooManyRequests) {
status = 429
if errors.Is(err, ErrUserInfo) {
status = 401
}
return &api.ErrorStatusCode{
StatusCode: status,

View File

@@ -1,105 +0,0 @@
package web_api
import (
"context"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/datastore"
"git.itzana.me/strafesnet/maps-service/pkg/model"
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
// GET /stats
func (svc *Service) GetStats(ctx context.Context) (*api.Stats, error) {
// Get total submissions count
totalSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, service.NewSubmissionFilter(), model.Page{
Number: 1,
Size: 0, // We only want the count, not the items
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get total mapfixes count
totalMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, service.NewMapfixFilter(), model.Page{
Number: 1,
Size: 0, // We only want the count, not the items
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get released submissions count
releasedSubmissionsFilter := service.NewSubmissionFilter()
releasedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatusReleased})
releasedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, releasedSubmissionsFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get released mapfixes count
releasedMapfixesFilter := service.NewMapfixFilter()
releasedMapfixesFilter.SetStatuses([]model.MapfixStatus{model.MapfixStatusReleased})
releasedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, releasedMapfixesFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get submitted submissions count (under review)
submittedSubmissionsFilter := service.NewSubmissionFilter()
submittedSubmissionsFilter.SetStatuses([]model.SubmissionStatus{
model.SubmissionStatusUnderConstruction,
model.SubmissionStatusChangesRequested,
model.SubmissionStatusSubmitting,
model.SubmissionStatusSubmitted,
model.SubmissionStatusAcceptedUnvalidated,
model.SubmissionStatusValidating,
model.SubmissionStatusValidated,
model.SubmissionStatusUploading,
model.SubmissionStatusUploaded,
})
submittedSubmissions, _, err := svc.inner.ListSubmissionsWithTotal(ctx, submittedSubmissionsFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
// Get submitted mapfixes count (under review)
submittedMapfixesFilter := service.NewMapfixFilter()
submittedMapfixesFilter.SetStatuses([]model.MapfixStatus{
model.MapfixStatusUnderConstruction,
model.MapfixStatusChangesRequested,
model.MapfixStatusSubmitting,
model.MapfixStatusSubmitted,
model.MapfixStatusAcceptedUnvalidated,
model.MapfixStatusValidating,
model.MapfixStatusValidated,
model.MapfixStatusUploading,
model.MapfixStatusUploaded,
model.MapfixStatusReleasing,
})
submittedMapfixes, _, err := svc.inner.ListMapfixesWithTotal(ctx, submittedMapfixesFilter, model.Page{
Number: 1,
Size: 0,
}, datastore.ListSortDisabled)
if err != nil {
return nil, err
}
return &api.Stats{
TotalSubmissions: totalSubmissions,
TotalMapfixes: totalMapfixes,
ReleasedSubmissions: releasedSubmissions,
ReleasedMapfixes: releasedMapfixes,
SubmittedSubmissions: submittedSubmissions,
SubmittedMapfixes: submittedMapfixes,
}, nil
}

View File

@@ -2,6 +2,7 @@ package web_api
import (
"context"
"errors"
"fmt"
"io"
"time"
@@ -12,35 +13,41 @@ import (
"git.itzana.me/strafesnet/maps-service/pkg/service"
)
var (
CreationPhaseSubmissionsLimit = 20
var(
CreationPhaseSubmissionsLimit = 20
CreationPhaseSubmissionStatuses = []model.SubmissionStatus{
model.SubmissionStatusChangesRequested,
model.SubmissionStatusSubmitted,
model.SubmissionStatusUnderConstruction,
}
// limit submissions in the pipeline to one per target map
ActiveAcceptedSubmissionStatuses = []model.SubmissionStatus{
model.SubmissionStatusUploading,
model.SubmissionStatusValidated,
model.SubmissionStatusValidating,
model.SubmissionStatusAcceptedUnvalidated,
}
// Allow 5 submissions every 10 minutes
CreateSubmissionRateLimit int64 = 5
CreateSubmissionRecencyWindow = time.Second * 600
CreateSubmissionRateLimit int64 = 5
CreateSubmissionRecencyWindow = time.Second*600
)
var (
ErrCreationPhaseSubmissionsLimit = fmt.Errorf("%w: Active submissions limited to 20", ErrPermissionDenied)
ErrUploadedAssetIDAlreadyExists = fmt.Errorf("%w: The submission UploadedAssetID is already set", ErrPermissionDenied)
ErrReleaseInvalidStatus = fmt.Errorf("%w: Only submissions with Uploaded status can be released", ErrPermissionDenied)
ErrReleaseNoUploadedAssetID = fmt.Errorf("%w: Only submissions with a UploadedAssetID can be released", ErrPermissionDenied)
ErrAcceptOwnSubmission = fmt.Errorf("%w: You cannot accept your own submission as the submitter", ErrPermissionDenied)
ErrCreateSubmissionRateLimit = fmt.Errorf("%w: You must not create more than 5 submissions every 10 minutes", ErrTooManyRequests)
ErrDisplayNameNotUnique = fmt.Errorf("%w: Cannot submit: A map exists with the same DisplayName", ErrPermissionDenied)
ErrCreationPhaseSubmissionsLimit = errors.New("Active submissions limited to 20")
ErrUploadedAssetIDAlreadyExists = errors.New("The submission UploadedAssetID is already set")
ErrReleaseInvalidStatus = errors.New("Only submissions with Uploaded status can be released")
ErrReleaseNoUploadedAssetID = errors.New("Only submissions with a UploadedAssetID can be released")
ErrAcceptOwnSubmission = fmt.Errorf("%w: You cannot accept your own submission as the submitter", ErrPermissionDenied)
ErrCreateSubmissionRateLimit = errors.New("You must not create more than 5 submissions every 10 minutes")
)
// POST /submissions
func (svc *Service) CreateSubmission(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) {
// sanitization
if request.AssetID < 0 {
if request.AssetID<0{
return nil, ErrNegativeID
}
var ModelID = uint64(request.AssetID)
var ModelID=uint64(request.AssetID);
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
@@ -60,7 +67,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 +93,8 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
}
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
Owner: userId,
StatusID: model.OperationStatusCreated,
Owner: userId,
StatusID: model.OperationStatusCreated,
})
if err != nil {
return nil, err
@@ -110,14 +117,13 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
OperationID: operation.ID,
}, nil
}
// POST /submissions-admin
func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.SubmissionTriggerCreate) (*api.OperationID, error) {
// sanitization
if request.AssetID < 0 {
if request.AssetID<0{
return nil, ErrNegativeID
}
var ModelID = uint64(request.AssetID)
var ModelID=uint64(request.AssetID);
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
@@ -135,7 +141,7 @@ func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.Subm
}
// check if caller has required role
has_role := roles&model.RolesSubmissionReview == model.RolesSubmissionReview
has_role := roles & model.RolesSubmissionReview == model.RolesSubmissionReview
if !has_role {
return nil, ErrPermissionDeniedNeedRoleSubmissionReview
}
@@ -156,8 +162,8 @@ func (svc *Service) CreateSubmissionAdmin(ctx context.Context, request *api.Subm
}
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
Owner: userId,
StatusID: model.OperationStatusCreated,
Owner: userId,
StatusID: model.OperationStatusCreated,
})
if err != nil {
return nil, err
@@ -192,18 +198,18 @@ func (svc *Service) GetSubmission(ctx context.Context, params api.GetSubmissionP
return nil, err
}
return &api.Submission{
ID: submission.ID,
DisplayName: submission.DisplayName,
Creator: submission.Creator,
GameID: int32(submission.GameID),
CreatedAt: submission.CreatedAt.Unix(),
UpdatedAt: submission.UpdatedAt.Unix(),
Submitter: int64(submission.Submitter),
AssetID: int64(submission.AssetID),
AssetVersion: int64(submission.AssetVersion),
Completed: submission.Completed,
ID: submission.ID,
DisplayName: submission.DisplayName,
Creator: submission.Creator,
GameID: int32(submission.GameID),
CreatedAt: submission.CreatedAt.Unix(),
UpdatedAt: submission.UpdatedAt.Unix(),
Submitter: int64(submission.Submitter),
AssetID: int64(submission.AssetID),
AssetVersion: int64(submission.AssetVersion),
Completed: submission.Completed,
UploadedAssetID: api.NewOptInt64(int64(submission.UploadedAssetID)),
StatusID: int32(submission.StatusID),
StatusID: int32(submission.StatusID),
}, nil
}
@@ -215,28 +221,25 @@ 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 {
filter.SetAssetVersion(uint64(asset_version))
}
if uploaded_asset_id, uploaded_asset_id_ok := params.UploadedAssetID.Get(); uploaded_asset_id_ok {
if uploaded_asset_id, uploaded_asset_id_ok := params.UploadedAssetID.Get(); uploaded_asset_id_ok{
filter.SetUploadedAssetID(uint64(uploaded_asset_id))
}
if status_id, status_id_ok := params.StatusID.Get(); status_id_ok {
if status_id, status_id_ok := params.StatusID.Get(); status_id_ok{
filter.SetStatuses([]model.SubmissionStatus{model.SubmissionStatus(status_id)})
}
@@ -245,27 +248,27 @@ func (svc *Service) ListSubmissions(ctx context.Context, params api.ListSubmissi
total, items, err := svc.inner.ListSubmissionsWithTotal(ctx, filter, model.Page{
Number: params.Page,
Size: params.Limit,
}, sort)
},sort)
if err != nil {
return nil, err
}
var resp api.Submissions
resp.Total = total
resp.Total=total
for _, item := range items {
resp.Submissions = append(resp.Submissions, api.Submission{
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: int32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: int64(item.Submitter),
AssetID: int64(item.AssetID),
AssetVersion: int64(item.AssetVersion),
Completed: item.Completed,
ID: item.ID,
DisplayName: item.DisplayName,
Creator: item.Creator,
GameID: int32(item.GameID),
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
Submitter: int64(item.Submitter),
AssetID: int64(item.AssetID),
AssetVersion: int64(item.AssetVersion),
Completed: item.Completed,
UploadedAssetID: api.NewOptInt64(int64(item.UploadedAssetID)),
StatusID: int32(item.StatusID),
StatusID: int32(item.StatusID),
})
}
@@ -342,9 +345,9 @@ func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.Update
}
event_data := model.AuditEventDataChangeModel{
OldModelID: OldModelID,
OldModelID: OldModelID,
OldModelVersion: OldModelVersion,
NewModelID: NewModelID,
NewModelID: NewModelID,
NewModelVersion: NewModelVersion,
}
@@ -352,7 +355,7 @@ func (svc *Service) UpdateSubmissionModel(ctx context.Context, params api.Update
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -402,7 +405,7 @@ func (svc *Service) ActionSubmissionReject(ctx context.Context, params api.Actio
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -438,12 +441,7 @@ func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params a
target_status := model.SubmissionStatusChangesRequested
update := service.NewSubmissionUpdate()
update.SetStatusID(target_status)
allowed_statuses := []model.SubmissionStatus{
model.SubmissionStatusUploaded,
model.SubmissionStatusValidated,
model.SubmissionStatusAcceptedUnvalidated,
model.SubmissionStatusSubmitted,
}
allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusValidated, model.SubmissionStatusAcceptedUnvalidated, model.SubmissionStatusSubmitted}
err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update)
if err != nil {
return err
@@ -457,7 +455,7 @@ func (svc *Service) ActionSubmissionRequestChanges(ctx context.Context, params a
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -510,7 +508,7 @@ func (svc *Service) ActionSubmissionRevoke(ctx context.Context, params api.Actio
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -553,37 +551,11 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
}
}
// check for maps with the exact same name
filter := service.NewMapFilter()
filter.SetDisplayName(submission.DisplayName)
maps_list, err := svc.inner.ListMaps(
ctx,
filter,
model.Page{
Number: 1,
Size: 1,
},
)
if err != nil {
return err
}
// The map search finds substrings, we only want exact matches
for _, m := range maps_list {
if m.DisplayName == submission.DisplayName {
return ErrDisplayNameNotUnique
}
}
// transaction
target_status := model.SubmissionStatusSubmitting
update := service.NewSubmissionUpdate()
update.SetStatusID(target_status)
allowed_statuses := []model.SubmissionStatus{
model.SubmissionStatusUnderConstruction,
model.SubmissionStatusChangesRequested,
model.SubmissionStatusSubmitted,
}
allowed_statuses := []model.SubmissionStatus{model.SubmissionStatusUnderConstruction, model.SubmissionStatusChangesRequested}
err = svc.inner.UpdateSubmissionIfStatus(ctx, params.SubmissionID, allowed_statuses, update)
if err != nil {
return err
@@ -606,7 +578,7 @@ func (svc *Service) ActionSubmissionTriggerSubmit(ctx context.Context, params ap
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -677,7 +649,7 @@ func (svc *Service) ActionSubmissionTriggerSubmitUnchecked(ctx context.Context,
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -705,7 +677,7 @@ func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params
if err != nil {
return err
}
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
// the last time the submission was updated must be longer than 10 seconds ago
return ErrDelayReset
}
@@ -734,7 +706,7 @@ func (svc *Service) ActionSubmissionResetSubmitting(ctx context.Context, params
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -802,7 +774,7 @@ func (svc *Service) ActionSubmissionTriggerUpload(ctx context.Context, params ap
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -839,7 +811,7 @@ func (svc *Service) ActionSubmissionValidated(ctx context.Context, params api.Ac
if err != nil {
return err
}
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
// the last time the submission was updated must be longer than 10 seconds ago
return ErrDelayReset
}
@@ -862,7 +834,7 @@ func (svc *Service) ActionSubmissionValidated(ctx context.Context, params api.Ac
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -934,7 +906,7 @@ func (svc *Service) ActionSubmissionTriggerValidate(ctx context.Context, params
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -994,7 +966,7 @@ func (svc *Service) ActionSubmissionRetryValidate(ctx context.Context, params ap
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -1031,7 +1003,7 @@ func (svc *Service) ActionSubmissionAccepted(ctx context.Context, params api.Act
if err != nil {
return err
}
if time.Now().Before(submission.UpdatedAt.Add(time.Second * 10)) {
if time.Now().Before(submission.UpdatedAt.Add(time.Second*10)) {
// the last time the submission was updated must be longer than 10 seconds ago
return ErrDelayReset
}
@@ -1054,7 +1026,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,
@@ -1066,24 +1038,19 @@ func (svc *Service) ActionSubmissionAccepted(ctx context.Context, params api.Act
// Release a set of uploaded maps.
//
// POST /release-submissions
func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.ReleaseInfo) (*api.OperationID, error) {
func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.ReleaseInfo) error {
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return nil, ErrUserInfo
return ErrUserInfo
}
has_role, err := userInfo.HasRoleSubmissionRelease()
if err != nil {
return nil, err
return err
}
// check if caller has required role
if !has_role {
return nil, ErrPermissionDeniedNeedRoleSubmissionRelease
}
userId, err := userInfo.GetUserID()
if err != nil {
return nil, err
return ErrPermissionDeniedNeedRoleSubmissionRelease
}
idList := make([]int64, len(request))
@@ -1094,70 +1061,56 @@ func (svc *Service) ReleaseSubmissions(ctx context.Context, request []api.Releas
// fetch submissions
submissions, err := svc.inner.GetSubmissionList(ctx, idList)
if err != nil {
return nil, err
return err
}
// the submissions are not ordered the same as the idList!
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 {
return nil, ErrReleaseInvalidStatus
for _,submission := range submissions{
if submission.StatusID != model.SubmissionStatusUploaded{
return ErrReleaseInvalidStatus
}
if submission.UploadedAssetID == 0 {
return nil, ErrReleaseNoUploadedAssetID
if submission.UploadedAssetID == 0{
return ErrReleaseNoUploadedAssetID
}
id_to_submission[submission.ID] = &submission
}
// construct batch release nats message
release_submissions := make([]model.ReleaseSubmissionRequest, len(request))
for i, release_info := range request {
// from request
release_submissions[i].ReleaseDate = release_info.Date.Unix()
release_submissions[i].SubmissionID = release_info.SubmissionID
submission := id_to_submission[release_info.SubmissionID]
// from submission
release_submissions[i].ModelID = submission.ValidatedAssetID
release_submissions[i].ModelVersion = submission.ValidatedAssetVersion
// for map create
release_submissions[i].UploadedAssetID = submission.UploadedAssetID
release_submissions[i].DisplayName = submission.DisplayName
release_submissions[i].Creator = submission.Creator
release_submissions[i].GameID = submission.GameID
release_submissions[i].Submitter = submission.Submitter
for i,submission := range submissions{
date := request[i].Date.Unix()
// create each map with go-grpc
_, err := svc.inner.CreateMap(ctx, model.Map{
ID: int64(submission.UploadedAssetID),
DisplayName: submission.DisplayName,
Creator: submission.Creator,
GameID: submission.GameID,
Date: time.Unix(date, 0),
Submitter: submission.Submitter,
// Thumbnail: 0,
// AssetVersion: 0,
// LoadCount: 0,
// Modes: 0,
})
if err != nil {
return err
}
// update each status to Released
update := service.NewSubmissionUpdate()
update.SetStatusID(model.SubmissionStatusReleased)
err = svc.inner.UpdateSubmissionIfStatus(ctx, submission.ID, []model.SubmissionStatus{model.SubmissionStatusUploaded}, update)
if err != nil {
return err
}
}
// create a trackable long-running operation
operation, err := svc.inner.CreateOperation(ctx, model.Operation{
Owner: userId,
StatusID: model.OperationStatusCreated,
})
if err != nil {
return nil, err
}
// this is a map fix
err = svc.inner.NatsBatchReleaseSubmissions(
release_submissions,
operation.ID,
)
if err != nil {
return nil, err
}
return &api.OperationID{
OperationID: operation.ID,
}, nil
return nil
}
// CreateSubmissionAuditComment implements createSubmissionAuditComment operation.
//
// # Post a comment to the audit log
// Post a comment to the audit log
//
// POST /submissions/{SubmissionID}/comment
func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.CreateSubmissionAuditCommentReq, params api.CreateSubmissionAuditCommentParams) error {
func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.CreateSubmissionAuditCommentReq, params api.CreateSubmissionAuditCommentParams) (error) {
userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return ErrUserInfo
@@ -1198,7 +1151,7 @@ func (svc *Service) CreateSubmissionAuditComment(ctx context.Context, req api.Cr
ctx,
userId,
model.Resource{
ID: params.SubmissionID,
ID: params.SubmissionID,
Type: model.ResourceSubmission,
},
event_data,
@@ -1214,7 +1167,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

@@ -1,135 +0,0 @@
package web_api
import (
"context"
"strconv"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
)
// BatchAssetThumbnails handles batch fetching of asset thumbnails
func (svc *Service) BatchAssetThumbnails(ctx context.Context, req *api.BatchAssetThumbnailsReq) (*api.BatchAssetThumbnailsOK, error) {
if len(req.AssetIds) == 0 {
return &api.BatchAssetThumbnailsOK{
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(map[string]string{}),
}, nil
}
// Convert size string to enum
size := roblox.Size420x420
if req.Size.IsSet() {
sizeStr := req.Size.Value
switch api.BatchAssetThumbnailsReqSize(sizeStr) {
case api.BatchAssetThumbnailsReqSize150x150:
size = roblox.Size150x150
case api.BatchAssetThumbnailsReqSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnails from service
thumbnails, err := svc.inner.GetAssetThumbnails(ctx, req.AssetIds, size)
if err != nil {
return nil, err
}
// Convert map[uint64]string to map[string]string for JSON
result := make(map[string]string, len(thumbnails))
for assetID, url := range thumbnails {
result[strconv.FormatUint(assetID, 10)] = url
}
return &api.BatchAssetThumbnailsOK{
Thumbnails: api.NewOptBatchAssetThumbnailsOKThumbnails(result),
}, nil
}
// GetAssetThumbnail handles single asset thumbnail fetch (with redirect)
func (svc *Service) GetAssetThumbnail(ctx context.Context, params api.GetAssetThumbnailParams) (*api.GetAssetThumbnailFound, error) {
// Convert size string to enum
size := roblox.Size420x420
if params.Size.IsSet() {
sizeStr := params.Size.Value
switch api.GetAssetThumbnailSize(sizeStr) {
case api.GetAssetThumbnailSize150x150:
size = roblox.Size150x150
case api.GetAssetThumbnailSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnail
thumbnailURL, err := svc.inner.GetSingleAssetThumbnail(ctx, params.AssetID, size)
if err != nil {
return nil, err
}
// Return redirect response
return &api.GetAssetThumbnailFound{
Location: api.NewOptString(thumbnailURL),
}, nil
}
// BatchUserThumbnails handles batch fetching of user avatar thumbnails
func (svc *Service) BatchUserThumbnails(ctx context.Context, req *api.BatchUserThumbnailsReq) (*api.BatchUserThumbnailsOK, error) {
if len(req.UserIds) == 0 {
return &api.BatchUserThumbnailsOK{
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(map[string]string{}),
}, nil
}
// Convert size string to enum
size := roblox.Size150x150
if req.Size.IsSet() {
sizeStr := req.Size.Value
switch api.BatchUserThumbnailsReqSize(sizeStr) {
case api.BatchUserThumbnailsReqSize420x420:
size = roblox.Size420x420
case api.BatchUserThumbnailsReqSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnails from service
thumbnails, err := svc.inner.GetUserAvatarThumbnails(ctx, req.UserIds, size)
if err != nil {
return nil, err
}
// Convert map[uint64]string to map[string]string for JSON
result := make(map[string]string, len(thumbnails))
for userID, url := range thumbnails {
result[strconv.FormatUint(userID, 10)] = url
}
return &api.BatchUserThumbnailsOK{
Thumbnails: api.NewOptBatchUserThumbnailsOKThumbnails(result),
}, nil
}
// GetUserThumbnail handles single user avatar thumbnail fetch (with redirect)
func (svc *Service) GetUserThumbnail(ctx context.Context, params api.GetUserThumbnailParams) (*api.GetUserThumbnailFound, error) {
// Convert size string to enum
size := roblox.Size150x150
if params.Size.IsSet() {
sizeStr := params.Size.Value
switch api.GetUserThumbnailSize(sizeStr) {
case api.GetUserThumbnailSize420x420:
size = roblox.Size420x420
case api.GetUserThumbnailSize768x432:
size = roblox.Size768x432
}
}
// Fetch thumbnail
thumbnailURL, err := svc.inner.GetSingleUserAvatarThumbnail(ctx, params.UserID, size)
if err != nil {
return nil, err
}
// Return redirect response
return &api.GetUserThumbnailFound{
Location: api.NewOptString(thumbnailURL),
}, nil
}

View File

@@ -1,33 +0,0 @@
package web_api
import (
"context"
"strconv"
"git.itzana.me/strafesnet/maps-service/pkg/api"
)
// BatchUsernames handles batch fetching of usernames
func (svc *Service) BatchUsernames(ctx context.Context, req *api.BatchUsernamesReq) (*api.BatchUsernamesOK, error) {
if len(req.UserIds) == 0 {
return &api.BatchUsernamesOK{
Usernames: api.NewOptBatchUsernamesOKUsernames(map[string]string{}),
}, nil
}
// Fetch usernames from service
usernames, err := svc.inner.GetUsernames(ctx, req.UserIds)
if err != nil {
return nil, err
}
// Convert map[uint64]string to map[string]string for JSON
result := make(map[string]string, len(usernames))
for userID, username := range usernames {
result[strconv.FormatUint(userID, 10)] = username
}
return &api.BatchUsernamesOK{
Usernames: api.NewOptBatchUsernamesOKUsernames(result),
}, nil
}

View File

@@ -4,18 +4,18 @@ version = "0.1.1"
edition = "2024"
[dependencies]
async-nats.workspace = true
futures-util.workspace = true
rbx_asset.workspace = true
rbx_binary.workspace = true
rbx_dom_weak.workspace = true
rbx_reflection_database = "2.0.1"
rbx_xml = "2.0.0"
regex = { version = "1.11.3", default-features = false }
serde.workspace = true
serde_json.workspace = true
async-nats = "0.42.0"
futures = "0.3.31"
rbx_asset = { version = "0.4.7", registry = "strafesnet" }
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection_database = "1.0.3"
rbx_xml = "1.0.0"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
siphasher = "1.0.1"
tokio.workspace = true
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "signal"] }
heck = "0.5.0"
rust-grpc = { version = "1.6.1", registry = "strafesnet" }
tonic = "0.14.1"
lazy-regex = "3.4.1"
rust-grpc = { version = "1.2.1", registry = "strafesnet" }
tonic = "0.13.1"

View File

@@ -1,4 +1,24 @@
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"]
# Using the `rust-musl-builder` as base image, instead of
# the official Rust toolchain
FROM registry.itzana.me/docker-proxy/clux/muslrust:1.86.0-stable AS chef
USER root
RUN cargo install cargo-chef
WORKDIR /app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
COPY api ./api
# Notice that we are specifying the --target flag!
RUN cargo chef cook --release --target x86_64-unknown-linux-musl --recipe-path recipe.json
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl --bin maps-validation
FROM registry.itzana.me/docker-proxy/alpine:3.21 AS runtime
RUN addgroup -S myuser && adduser -S myuser -G myuser
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/maps-validation /usr/local/bin/
USER myuser
ENTRYPOINT ["/usr/local/bin/maps-validation"]

View File

@@ -1,6 +1,6 @@
[package]
name = "submissions-api"
version = "0.10.1"
version = "0.8.2"
edition = "2024"
publish = ["strafesnet"]
repository = "https://git.itzana.me/StrafesNET/maps-service"
@@ -12,12 +12,8 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
chrono = { version = "0.4.41", features = ["serde"] }
reqwest = { version = "0", features = [
"json", "rustls-tls",
# default features
"charset", "http2", "system-proxy"
], default-features = false }
serde.workspace = true
serde_json.workspace = true
reqwest = { version = "0", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_repr = "0.1.19"
url = "2"

View File

@@ -152,48 +152,6 @@ impl Context{
Ok(())
}
pub async fn get_mapfixes(&self,config:GetMapfixesRequest<'_>)->Result<MapfixesResponse,Error>{
let url_raw=format!("{}/mapfixes",self.0.base_url);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?;
{
let mut query_pairs=url.query_pairs_mut();
query_pairs.append_pair("Page",config.Page.to_string().as_str());
query_pairs.append_pair("Limit",config.Limit.to_string().as_str());
if let Some(sort)=config.Sort{
query_pairs.append_pair("Sort",(sort as u8).to_string().as_str());
}
if let Some(display_name)=config.DisplayName{
query_pairs.append_pair("DisplayName",display_name);
}
if let Some(creator)=config.Creator{
query_pairs.append_pair("Creator",creator);
}
if let Some(game_id)=config.GameID{
query_pairs.append_pair("GameID",(game_id as u8).to_string().as_str());
}
if let Some(submitter)=config.Submitter{
query_pairs.append_pair("Submitter",submitter.to_string().as_str());
}
if let Some(asset_id)=config.AssetID{
query_pairs.append_pair("AssetID",asset_id.to_string().as_str());
}
if let Some(asset_version)=config.AssetVersion{
query_pairs.append_pair("AssetVersion",asset_version.to_string().as_str());
}
if let Some(uploaded_asset_id)=config.TargetAssetID{
query_pairs.append_pair("TargetAssetID",uploaded_asset_id.to_string().as_str());
}
if let Some(status_id)=config.StatusID{
query_pairs.append_pair("StatusID",(status_id as u8).to_string().as_str());
}
}
response_ok(
self.0.get(url).await.map_err(Error::Reqwest)?
).await.map_err(Error::Response)?
.json().await.map_err(Error::ReqwestJson)
}
pub async fn get_submissions(&self,config:GetSubmissionsRequest<'_>)->Result<SubmissionsResponse,Error>{
let url_raw=format!("{}/submissions",self.0.base_url);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?;
@@ -220,9 +178,6 @@ impl Context{
if let Some(asset_id)=config.AssetID{
query_pairs.append_pair("AssetID",asset_id.to_string().as_str());
}
if let Some(asset_version)=config.AssetVersion{
query_pairs.append_pair("AssetVersion",asset_version.to_string().as_str());
}
if let Some(uploaded_asset_id)=config.UploadedAssetID{
query_pairs.append_pair("UploadedAssetID",uploaded_asset_id.to_string().as_str());
}
@@ -263,37 +218,7 @@ impl Context{
).await.map_err(Error::Response)?
.json().await.map_err(Error::ReqwestJson)
}
pub async fn get_mapfix_audit_events(&self,config:GetMapfixAuditEventsRequest)->Result<Vec<AuditEventReponse>,Error>{
let url_raw=format!("{}/mapfixes/{}/audit-events",self.0.base_url,config.MapfixID);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?;
{
let mut query_pairs=url.query_pairs_mut();
query_pairs.append_pair("Page",config.Page.to_string().as_str());
query_pairs.append_pair("Limit",config.Limit.to_string().as_str());
}
response_ok(
self.0.get(url).await.map_err(Error::Reqwest)?
).await.map_err(Error::Response)?
.json().await.map_err(Error::ReqwestJson)
}
pub async fn get_submission_audit_events(&self,config:GetSubmissionAuditEventsRequest)->Result<Vec<AuditEventReponse>,Error>{
let url_raw=format!("{}/submissions/{}/audit-events",self.0.base_url,config.SubmissionID);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?;
{
let mut query_pairs=url.query_pairs_mut();
query_pairs.append_pair("Page",config.Page.to_string().as_str());
query_pairs.append_pair("Limit",config.Limit.to_string().as_str());
}
response_ok(
self.0.get(url).await.map_err(Error::Reqwest)?
).await.map_err(Error::Response)?
.json().await.map_err(Error::ReqwestJson)
}
pub async fn release_submissions(&self,config:ReleaseRequest<'_>)->Result<OperationIDResponse,Error>{
pub async fn release_submissions(&self,config:ReleaseRequest<'_>)->Result<(),Error>{
let url_raw=format!("{}/release-submissions",self.0.base_url);
let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::Parse)?;
@@ -301,7 +226,8 @@ impl Context{
response_ok(
self.0.post(url,body).await.map_err(Error::Reqwest)?
).await.map_err(Error::Response)?
.json().await.map_err(Error::ReqwestJson)
).await.map_err(Error::Response)?;
Ok(())
}
}

View File

@@ -30,6 +30,7 @@ impl<Items> std::error::Error for SingleItemError<Items> where Items:std::fmt::D
pub type ScriptSingleItemError=SingleItemError<Vec<ScriptID>>;
pub type ScriptPolicySingleItemError=SingleItemError<Vec<ScriptPolicyID>>;
#[allow(dead_code)]
#[derive(Debug)]
pub struct UrlAndBody{
pub url:url::Url,
@@ -75,7 +76,7 @@ pub enum GameID{
FlyTrials=5,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct CreateMapfixRequest<'a>{
pub OperationID:OperationID,
@@ -88,13 +89,13 @@ pub struct CreateMapfixRequest<'a>{
pub TargetAssetID:u64,
pub Description:&'a str,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct MapfixIDResponse{
pub MapfixID:MapfixID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct CreateSubmissionRequest<'a>{
pub OperationID:OperationID,
@@ -107,7 +108,7 @@ pub struct CreateSubmissionRequest<'a>{
pub Status:u32,
pub Roles:u32,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct SubmissionIDResponse{
pub SubmissionID:SubmissionID,
@@ -126,11 +127,11 @@ pub enum ResourceType{
Submission=2,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
pub struct GetScriptRequest{
pub ScriptID:ScriptID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct GetScriptsRequest<'a>{
pub Page:u32,
@@ -150,7 +151,7 @@ pub struct GetScriptsRequest<'a>{
pub struct HashRequest<'a>{
pub hash:&'a str,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptResponse{
pub ID:ScriptID,
@@ -160,7 +161,7 @@ pub struct ScriptResponse{
pub ResourceType:ResourceType,
pub ResourceID:ResourceID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct CreateScriptRequest<'a>{
pub Name:&'a str,
@@ -169,7 +170,7 @@ pub struct CreateScriptRequest<'a>{
#[serde(skip_serializing_if="Option::is_none")]
pub ResourceID:Option<ResourceID>,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptIDResponse{
pub ScriptID:ScriptID,
@@ -185,11 +186,11 @@ pub enum Policy{
Replace=4,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
pub struct GetScriptPolicyRequest{
pub ScriptPolicyID:ScriptPolicyID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct GetScriptPoliciesRequest<'a>{
pub Page:u32,
@@ -201,7 +202,7 @@ pub struct GetScriptPoliciesRequest<'a>{
#[serde(skip_serializing_if="Option::is_none")]
pub Policy:Option<Policy>,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptPolicyResponse{
pub ID:ScriptPolicyID,
@@ -209,20 +210,20 @@ pub struct ScriptPolicyResponse{
pub ToScriptID:ScriptID,
pub Policy:Policy
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct CreateScriptPolicyRequest{
pub FromScriptID:ScriptID,
pub ToScriptID:ScriptID,
pub Policy:Policy,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct ScriptPolicyIDResponse{
pub ScriptPolicyID:ScriptPolicyID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct UpdateScriptPolicyRequest{
pub ID:ScriptPolicyID,
@@ -234,7 +235,7 @@ pub struct UpdateScriptPolicyRequest{
pub Policy:Option<Policy>,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct UpdateSubmissionModelRequest{
pub SubmissionID:SubmissionID,
@@ -251,73 +252,6 @@ pub enum Sort{
DateDescending=4,
}
#[derive(Clone,Debug,serde_repr::Serialize_repr,serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum MapfixStatus{
// Phase: Creation
UnderConstruction=0,
ChangesRequested=1,
// Phase: Review
Submitting=2,
Submitted=3,
// Phase: Testing
AcceptedUnvalidated=4, // pending script review, can re-trigger validation
Validating=5,
Validated=6,
Uploading=7,
Uploaded=8, // uploaded to the group, but pending release
Releasing=11,
// Phase: Final MapfixStatus
Rejected=9,
Released=10,
}
#[expect(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct GetMapfixesRequest<'a>{
pub Page:u32,
pub Limit:u32,
pub Sort:Option<Sort>,
pub DisplayName:Option<&'a str>,
pub Creator:Option<&'a str>,
pub GameID:Option<GameID>,
pub Submitter:Option<u64>,
pub AssetID:Option<u64>,
pub AssetVersion:Option<u64>,
pub TargetAssetID:Option<u64>,
pub StatusID:Option<MapfixStatus>,
}
#[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
pub struct MapfixResponse{
pub ID:MapfixID,
pub DisplayName:String,
pub Creator:String,
pub GameID:u32,
pub CreatedAt:i64,
pub UpdatedAt:i64,
pub Submitter:u64,
pub AssetID:u64,
pub AssetVersion:u64,
pub ValidatedAssetID:Option<u64>,
pub ValidatedAssetVersion:Option<u64>,
pub Completed:bool,
pub TargetAssetID:u64,
pub StatusID:MapfixStatus,
pub Description:String,
}
#[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct MapfixesResponse{
pub Total:u64,
pub Mapfixes:Vec<MapfixResponse>,
}
#[derive(Clone,Debug,serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum SubmissionStatus{
@@ -341,7 +275,7 @@ pub enum SubmissionStatus{
Released=10,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct GetSubmissionsRequest<'a>{
pub Page:u32,
@@ -352,12 +286,11 @@ pub struct GetSubmissionsRequest<'a>{
pub GameID:Option<GameID>,
pub Submitter:Option<u64>,
pub AssetID:Option<u64>,
pub AssetVersion:Option<u64>,
pub UploadedAssetID:Option<u64>,
pub StatusID:Option<SubmissionStatus>,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct SubmissionResponse{
pub ID:SubmissionID,
@@ -369,20 +302,18 @@ pub struct SubmissionResponse{
pub Submitter:u64,
pub AssetID:u64,
pub AssetVersion:u64,
pub ValidatedAssetID:Option<u64>,
pub ValidatedAssetVersion:Option<u64>,
pub UploadedAssetID:u64,
pub StatusID:SubmissionStatus,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct SubmissionsResponse{
pub Total:u64,
pub Submissions:Vec<SubmissionResponse>,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct GetMapsRequest<'a>{
pub Page:u32,
@@ -393,7 +324,7 @@ pub struct GetMapsRequest<'a>{
pub GameID:Option<GameID>,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct MapResponse{
pub ID:i64,
@@ -403,119 +334,7 @@ pub struct MapResponse{
pub Date:i64,
}
#[expect(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct GetMapfixAuditEventsRequest{
pub Page:u32,
pub Limit:u32,
pub MapfixID:i64,
}
#[expect(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct GetSubmissionAuditEventsRequest{
pub Page:u32,
pub Limit:u32,
pub SubmissionID:i64,
}
#[derive(Clone,Debug,serde_repr::Deserialize_repr)]
#[repr(u32)]
pub enum AuditEventType{
Action=0,
Comment=1,
ChangeModel=2,
ChangeValidatedModel=3,
ChangeDisplayName=4,
ChangeCreator=5,
Error=6,
CheckList=7,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventAction{
pub target_status:MapfixStatus,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventComment{
pub comment:String,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventChangeModel{
pub old_model_id:u64,
pub old_model_version:u64,
pub new_model_id:u64,
pub new_model_version:u64,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventChangeValidatedModel{
pub validated_model_id:u64,
pub validated_model_version:u64,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventChangeName{
pub old_name:String,
pub new_name:String,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventError{
pub error:String,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventCheck{
pub name:String,
pub summary:String,
pub passed:bool,
}
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventCheckList{
pub check_list:Vec<AuditEventCheck>,
}
#[derive(Clone,Debug)]
pub enum AuditEventData{
Action(AuditEventAction),
Comment(AuditEventComment),
ChangeModel(AuditEventChangeModel),
ChangeValidatedModel(AuditEventChangeValidatedModel),
ChangeDisplayName(AuditEventChangeName),
ChangeCreator(AuditEventChangeName),
Error(AuditEventError),
CheckList(AuditEventCheckList),
}
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
pub struct AuditEventID(pub(crate)i64);
#[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct AuditEventReponse{
pub ID:AuditEventID,
pub Date:i64,
pub User:u64,
pub Username:String,
pub ResourceType:ResourceType,
pub ResourceID:ResourceID,
pub EventType:AuditEventType,
EventData:serde_json::Value,
}
impl AuditEventReponse{
pub fn data(self)->serde_json::Result<AuditEventData>{
Ok(match self.EventType{
AuditEventType::Action=>AuditEventData::Action(serde_json::from_value(self.EventData)?),
AuditEventType::Comment=>AuditEventData::Comment(serde_json::from_value(self.EventData)?),
AuditEventType::ChangeModel=>AuditEventData::ChangeModel(serde_json::from_value(self.EventData)?),
AuditEventType::ChangeValidatedModel=>AuditEventData::ChangeValidatedModel(serde_json::from_value(self.EventData)?),
AuditEventType::ChangeDisplayName=>AuditEventData::ChangeDisplayName(serde_json::from_value(self.EventData)?),
AuditEventType::ChangeCreator=>AuditEventData::ChangeCreator(serde_json::from_value(self.EventData)?),
AuditEventType::Error=>AuditEventData::Error(serde_json::from_value(self.EventData)?),
AuditEventType::CheckList=>AuditEventData::CheckList(serde_json::from_value(self.EventData)?),
})
}
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct Check{
pub Name:&'static str,
@@ -523,7 +342,7 @@ pub struct Check{
pub Passed:bool,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionSubmissionSubmittedRequest{
pub SubmissionID:SubmissionID,
@@ -533,33 +352,33 @@ pub struct ActionSubmissionSubmittedRequest{
pub GameID:GameID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionSubmissionRequestChangesRequest{
pub SubmissionID:SubmissionID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionSubmissionUploadedRequest{
pub SubmissionID:SubmissionID,
pub UploadedAssetID:u64,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionSubmissionAcceptedRequest{
pub SubmissionID:SubmissionID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct CreateSubmissionAuditErrorRequest{
pub SubmissionID:SubmissionID,
pub ErrorMessage:String,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct CreateSubmissionAuditCheckListRequest<'a>{
pub SubmissionID:SubmissionID,
@@ -568,16 +387,8 @@ pub struct CreateSubmissionAuditCheckListRequest<'a>{
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
pub struct SubmissionID(pub(crate)i64);
impl SubmissionID{
pub const fn new(value:i64)->Self{
Self(value)
}
pub const fn value(&self)->i64{
self.0
}
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct UpdateMapfixModelRequest{
pub MapfixID:MapfixID,
@@ -585,7 +396,7 @@ pub struct UpdateMapfixModelRequest{
pub ModelVersion:u64,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionMapfixSubmittedRequest{
pub MapfixID:MapfixID,
@@ -595,32 +406,32 @@ pub struct ActionMapfixSubmittedRequest{
pub GameID:GameID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionMapfixRequestChangesRequest{
pub MapfixID:MapfixID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionMapfixUploadedRequest{
pub MapfixID:MapfixID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionMapfixAcceptedRequest{
pub MapfixID:MapfixID,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct CreateMapfixAuditErrorRequest{
pub MapfixID:MapfixID,
pub ErrorMessage:String,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct CreateMapfixAuditCheckListRequest<'a>{
pub MapfixID:MapfixID,
@@ -629,16 +440,8 @@ pub struct CreateMapfixAuditCheckListRequest<'a>{
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,serde::Serialize,serde::Deserialize)]
pub struct MapfixID(pub(crate)i64);
impl MapfixID{
pub const fn new(value:i64)->Self{
Self(value)
}
pub const fn value(&self)->i64{
self.0
}
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug)]
pub struct ActionOperationFailedRequest{
pub OperationID:OperationID,
@@ -665,7 +468,7 @@ impl Resource{
}
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
#[derive(Clone,Debug,serde::Serialize)]
pub struct ReleaseInfo{
pub SubmissionID:SubmissionID,
@@ -675,8 +478,3 @@ pub struct ReleaseInfo{
pub struct ReleaseRequest<'a>{
pub schedule:&'a [ReleaseInfo],
}
#[expect(nonstandard_style)]
#[derive(Clone,Debug,serde::Deserialize)]
pub struct OperationIDResponse{
pub OperationID:OperationID,
}

View File

@@ -6,7 +6,7 @@ use heck::{ToSnakeCase,ToTitleCase};
use rbx_dom_weak::Instance;
use rust_grpc::validator::Check;
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
ModelInfoDownload(rbx_asset::cloud::GetError),
@@ -24,16 +24,7 @@ impl std::fmt::Display for Error{
}
impl std::error::Error for Error{}
macro_rules! lazy_regex{
($r:literal)=>{{
use regex::Regex;
use std::sync::LazyLock;
static RE:LazyLock<Regex>=LazyLock::new(||Regex::new($r).unwrap());
&RE
}};
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
pub struct CheckRequest{
ModelID:u64,
SkipChecks:bool,
@@ -56,20 +47,12 @@ impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{
}
}
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq,Ord,PartialOrd)]
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
struct ModeID(u64);
impl ModeID{
const MAIN:Self=Self(0);
const BONUS:Self=Self(1);
}
impl std::fmt::Display for ModeID{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
match self{
&ModeID::MAIN=>write!(f,"Main"),
&ModeID(mode_id)=>write!(f,"Bonus{mode_id}"),
}
}
}
enum Zone{
Start,
Finish,
@@ -79,7 +62,7 @@ struct ModeElement{
zone:Zone,
mode_id:ModeID,
}
#[expect(dead_code)]
#[allow(dead_code)]
pub enum IDParseError{
NoCaptures,
ParseInt(core::num::ParseIntError),
@@ -96,7 +79,7 @@ impl std::str::FromStr for ModeElement{
"BonusFinish"=>Ok(Self{zone:Zone::Finish,mode_id:ModeID::BONUS}),
"BonusAnticheat"=>Ok(Self{zone:Zone::Anticheat,mode_id:ModeID::BONUS}),
other=>{
let everything_pattern=lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$|^Bonus(\d+)Finish$|^BonusFinish(\d+)$|^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
let everything_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$|^Bonus(\d+)Finish$|^BonusFinish(\d+)$|^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
if let Some(captures)=everything_pattern.captures(other){
if let Some(mode_id)=captures.get(1).or(captures.get(2)){
return Ok(Self{
@@ -156,16 +139,16 @@ impl std::str::FromStr for StageElement{
type Err=IDParseError;
fn from_str(s:&str)->Result<Self,Self::Err>{
// Trigger ForceTrigger Teleport ForceTeleport SpawnAt ForceSpawnAt
let teleport_pattern=lazy_regex!(r"^(?:Force)?(Teleport|SpawnAt|Trigger)(\d+)$");
if let Some(captures)=teleport_pattern.captures(s){
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^(?:Force)?(Teleport|SpawnAt|Trigger)(\d+)$");
if let Some(captures)=bonus_start_pattern.captures(s){
return Ok(StageElement{
behaviour:StageElementBehaviour::Teleport,
stage_id:StageID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
}
// Spawn
let spawn_pattern=lazy_regex!(r"^Spawn(\d+)$");
if let Some(captures)=spawn_pattern.captures(s){
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$");
if let Some(captures)=bonus_finish_pattern.captures(s){
return Ok(StageElement{
behaviour:StageElementBehaviour::Spawn,
stage_id:StageID(captures[1].parse().map_err(IDParseError::ParseInt)?),
@@ -197,15 +180,15 @@ struct WormholeElement{
impl std::str::FromStr for WormholeElement{
type Err=IDParseError;
fn from_str(s:&str)->Result<Self,Self::Err>{
let wormhole_in_pattern=lazy_regex!(r"^WormholeIn(\d+)$");
if let Some(captures)=wormhole_in_pattern.captures(s){
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^WormholeIn(\d+)$");
if let Some(captures)=bonus_start_pattern.captures(s){
return Ok(Self{
behaviour:WormholeBehaviour::In,
wormhole_id:WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
}
let wormhole_out_pattern=lazy_regex!(r"^WormholeOut(\d+)$");
if let Some(captures)=wormhole_out_pattern.captures(s){
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$");
if let Some(captures)=bonus_finish_pattern.captures(s){
return Ok(Self{
behaviour:WormholeBehaviour::Out,
wormhole_id:WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
@@ -223,15 +206,6 @@ impl std::fmt::Display for WormholeElement{
}
}
fn count_sequential(modes:&HashMap<ModeID,Vec<&Instance>>)->usize{
for mode_id in 0..modes.len(){
if !modes.contains_key(&ModeID(mode_id as u64)){
return mode_id;
}
}
return modes.len();
}
/// Count various map elements
#[derive(Default)]
struct Counts<'a>{
@@ -251,24 +225,6 @@ pub struct ModelInfo<'a>{
counts:Counts<'a>,
unanchored_parts:Vec<&'a Instance>,
}
impl ModelInfo<'_>{
pub fn count_modes(&self)->Option<usize>{
let start_zones_count=self.counts.mode_start_counts.len();
let finish_zones_count=self.counts.mode_finish_counts.len();
let sequential_start_zones=count_sequential(&self.counts.mode_start_counts);
let sequential_finish_zones=count_sequential(&self.counts.mode_finish_counts);
// all counts must match
if start_zones_count==finish_zones_count
&& sequential_start_zones==sequential_finish_zones
&& start_zones_count==sequential_start_zones
&& finish_zones_count==sequential_finish_zones
{
Some(start_zones_count)
}else{
None
}
}
}
pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_dom_weak::Instance)->ModelInfo<'a>{
// extract model info
@@ -281,7 +237,7 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
let mut unanchored_parts=Vec::new();
let anchored_ustr=rbx_dom_weak::ustr("Anchored");
let db=rbx_reflection_database::get().unwrap();
let db=rbx_reflection_database::get();
let base_part=&db.classes["BasePart"];
let base_parts=dom.descendants_of(model_instance.referent()).filter(|&instance|
db.classes.get(instance.class.as_str()).is_some_and(|class|
@@ -324,24 +280,25 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
}
// check if an observed string matches an expected string
pub struct StringEquality<'a,Str>{
pub struct StringCheck<'a,T,Str>(Result<T,StringCheckContext<'a,Str>>);
pub struct StringCheckContext<'a,Str>{
observed:&'a str,
expected:Str,
}
impl<'a,Str> StringEquality<'a,Str>
impl<'a,Str> StringCheckContext<'a,Str>
where
&'a str:PartialEq<Str>,
{
/// Compute the StringCheck, passing through the provided value on success.
fn check<T>(self,value:T)->Result<T,Self>{
fn check<T>(self,value:T)->StringCheck<'a,T,Str>{
if self.observed==self.expected{
Ok(value)
StringCheck(Ok(value))
}else{
Err(self)
StringCheck(Err(self))
}
}
}
impl<Str:std::fmt::Display> std::fmt::Display for StringEquality<'_,Str>{
impl<Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'_,Str>{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"expected: {}, observed: {}",self.expected,self.observed)
}
@@ -441,7 +398,7 @@ pub struct MapInfoOwned{
pub creator:String,
pub game_id:GameID,
}
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum IntoMapInfoOwnedError{
DisplayName(StringValueError),
@@ -463,66 +420,19 @@ impl TryFrom<MapInfo<'_>> for MapInfoOwned{
struct Exists;
struct Absent;
enum DisplayNameError<'a>{
TitleCase(StringEquality<'a,String>),
Empty(StringEmpty),
TooLong(usize),
StringValue(StringValueError),
}
fn check_display_name<'a>(display_name:Result<&'a str,StringValueError>)->Result<&'a str,DisplayNameError<'a>>{
// DisplayName StringValue can be missing or whatever
let display_name=display_name.map_err(DisplayNameError::StringValue)?;
// DisplayName cannot be ""
let display_name=check_empty(display_name).map_err(DisplayNameError::Empty)?;
// DisplayName cannot exceed 50 characters
if 50<display_name.len(){
return Err(DisplayNameError::TooLong(display_name.len()));
}
// Check title case
let display_name=StringEquality{
observed:display_name,
expected:display_name.to_title_case(),
}.check(display_name).map_err(DisplayNameError::TitleCase)?;
Ok(display_name)
}
enum CreatorError{
Empty(StringEmpty),
TooLong(usize),
StringValue(StringValueError),
}
fn check_creator<'a>(creator:Result<&'a str,StringValueError>)->Result<&'a str,CreatorError>{
// Creator StringValue can be missing or whatever
let creator=creator.map_err(CreatorError::StringValue)?;
// Creator cannot be ""
let creator=check_empty(creator).map_err(CreatorError::Empty)?;
// Creator cannot exceed 50 characters
if 50<creator.len(){
return Err(CreatorError::TooLong(creator.len()));
}
Ok(creator)
}
/// The result of every map check.
struct MapCheck<'a>{
// === METADATA CHECKS ===
// The root must be of class Model
model_class:Result<(),StringEquality<'a,&'static str>>,
model_class:StringCheck<'a,(),&'static str>,
// Model's name must be in snake case
model_name:Result<(),StringEquality<'a,String>>,
model_name:StringCheck<'a,(),String>,
// Map must have a StringValue named DisplayName.
// Value must not be empty, must be in title case.
display_name:Result<&'a str,DisplayNameError<'a>>,
display_name:Result<Result<StringCheck<'a,&'a str,String>,StringEmpty>,StringValueError>,
// Map must have a StringValue named Creator.
// Value must not be empty.
creator:Result<&'a str,CreatorError>,
creator:Result<Result<&'a str,StringEmpty>,StringValueError>,
// The prefix of the model's name must match the game it was submitted for.
// bhop_ for bhop, and surf_ for surf
game_id:Result<GameID,ParseGameIDError>,
@@ -536,8 +446,6 @@ struct MapCheck<'a>{
mode_finish_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<ModeID,Vec<&'a Instance>>>,
// Check for dangling MapAnticheat zones (no associated MapStart)
mode_anticheat_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<ModeID,Vec<&'a Instance>>>,
// Check that modes are sequential
modes_sequential:Result<(),Vec<ModeID>>,
// Spawn1 must exist
spawn1:Result<Exists,Absent>,
// Check for dangling Teleport# (no associated Spawn#)
@@ -557,22 +465,27 @@ struct MapCheck<'a>{
impl<'a> ModelInfo<'a>{
fn check(self)->MapCheck<'a>{
// Check class is exactly "Model"
let model_class=StringEquality{
let model_class=StringCheckContext{
observed:self.model_class,
expected:"Model",
}.check(());
// Check model name is snake case
let model_name=StringEquality{
let model_name=StringCheckContext{
observed:self.model_name,
expected:self.model_name.to_snake_case(),
}.check(());
// Check display name is not empty and has title case
let display_name=check_display_name(self.map_info.display_name);
let display_name=self.map_info.display_name.map(|display_name|{
check_empty(display_name).map(|display_name|StringCheckContext{
observed:display_name,
expected:display_name.to_title_case(),
}.check(display_name))
});
// Check Creator is not empty
let creator=check_creator(self.map_info.creator);
let creator=self.map_info.creator.map(check_empty);
// Check GameID (model name was prefixed with bhop_ surf_ etc)
let game_id=self.map_info.game_id;
@@ -601,25 +514,6 @@ impl<'a> ModelInfo<'a>{
let mode_anticheat_counts=SetDifferenceCheckContextAllowNone::new(self.counts.mode_anticheat_counts)
.check(&self.counts.mode_start_counts);
// There must not be non-sequential modes. If Bonus100 exists, Bonuses 1-99 had better also exist.
let modes_sequential={
let sequential=count_sequential(&self.counts.mode_start_counts);
if sequential==self.counts.mode_start_counts.len(){
Ok(())
}else{
let mut non_sequential=Vec::with_capacity(self.counts.mode_start_counts.len()-sequential);
for (&mode_id,_) in &self.counts.mode_start_counts{
let ModeID(mode_id_u64)=mode_id;
if !(mode_id_u64<sequential as u64){
non_sequential.push(mode_id);
}
}
// sort so it's prettier when it prints out
non_sequential.sort();
Err(non_sequential)
}
};
// There must be exactly one start zone for every mode in the map.
let mode_start_counts=DuplicateCheckContext(self.counts.mode_start_counts).check(|c|1<c.len());
@@ -656,7 +550,6 @@ impl<'a> ModelInfo<'a>{
mode_start_counts,
mode_finish_counts,
mode_anticheat_counts,
modes_sequential,
spawn1,
teleport_counts,
spawn_counts,
@@ -671,16 +564,15 @@ impl MapCheck<'_>{
fn result(self)->Result<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
match self{
MapCheck{
model_class:Ok(()),
model_name:Ok(()),
display_name:Ok(display_name),
creator:Ok(creator),
model_class:StringCheck(Ok(())),
model_name:StringCheck(Ok(())),
display_name:Ok(Ok(StringCheck(Ok(display_name)))),
creator:Ok(Ok(creator)),
game_id:Ok(game_id),
mapstart:Ok(Exists),
mode_start_counts:DuplicateCheck(Ok(())),
mode_finish_counts:SetDifferenceCheck(Ok(())),
mode_anticheat_counts:SetDifferenceCheck(Ok(())),
modes_sequential:Ok(()),
spawn1:Ok(Exists),
teleport_counts:SetDifferenceCheck(Ok(())),
spawn_counts:DuplicateCheck(Ok(())),
@@ -778,25 +670,27 @@ macro_rules! summary_format{
impl MapCheck<'_>{
fn itemize(&self)->Result<MapCheckList,serde_json::Error>{
let model_class=match &self.model_class{
Ok(())=>passed!("ModelClass"),
Err(context)=>summary_format!("ModelClass","Invalid model class: {context}"),
StringCheck(Ok(()))=>passed!("ModelClass"),
StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}"),
};
let model_name=match &self.model_name{
Ok(())=>passed!("ModelName"),
Err(context)=>summary_format!("ModelName","Model name must have snake_case: {context}"),
StringCheck(Ok(()))=>passed!("ModelName"),
StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}"),
};
let display_name=match &self.display_name{
Ok(_)=>passed!("DisplayName"),
Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
Err(DisplayNameError::TooLong(context))=>summary_format!("DisplayName","DisplayName is too long: {context} characters (50 characters max)"),
Err(DisplayNameError::StringValue(context))=>summary_format!("DisplayName","DisplayName StringValue: {context}"),
Ok(Ok(StringCheck(Ok(_))))=>passed!("DisplayName"),
Ok(Ok(StringCheck(Err(context))))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"),
Ok(Err(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"),
Err(StringValueError::ObjectNotFound)=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()),
Err(StringValueError::ValueNotSet)=>summary!("DisplayName","DisplayName Value not set".to_owned()),
Err(StringValueError::NonStringValue)=>summary!("DisplayName","DisplayName Value is not a String".to_owned()),
};
let creator=match &self.creator{
Ok(_)=>passed!("Creator"),
Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"),
Err(CreatorError::TooLong(context))=>summary_format!("Creator","Creator is too long: {context} characters (50 characters max)"),
Err(CreatorError::StringValue(context))=>summary_format!("Creator","Creator StringValue: {context}"),
Ok(Ok(_))=>passed!("Creator"),
Ok(Err(context))=>summary_format!("Creator","Invalid Creator: {context}"),
Err(StringValueError::ObjectNotFound)=>summary!("Creator","Missing Creator StringValue".to_owned()),
Err(StringValueError::ValueNotSet)=>summary!("Creator","Creator Value not set".to_owned()),
Err(StringValueError::NonStringValue)=>summary!("Creator","Creator Value is not a String".to_owned()),
};
let game_id=match &self.game_id{
Ok(_)=>passed!("GameID"),
@@ -852,15 +746,6 @@ impl MapCheck<'_>{
}
}
};
let sequential_modes=match &self.modes_sequential{
Ok(())=>passed!("SequentialModes"),
Err(context)=>{
let non_sequential=context.len();
let plural_non_sequential=if non_sequential==1{"mode"}else{"modes"};
let comma_separated=Separated::new(", ",||context);
summary_format!("SequentialModes","{non_sequential} {plural_non_sequential} should use a lower ModeID (no gaps): {comma_separated}")
}
};
let spawn1=match &self.spawn1{
Ok(Exists)=>passed!("Spawn1"),
Err(Absent)=>summary_format!("Spawn1","Model has no Spawn1"),
@@ -939,7 +824,6 @@ impl MapCheck<'_>{
extra_finish,
missing_finish,
dangling_anticheat,
sequential_modes,
spawn1,
dangling_teleport,
duplicate_spawns,

View File

@@ -1,7 +1,7 @@
use crate::check::CheckListAndVersion;
use crate::nats_types::CheckMapfixRequest;
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Check(crate::check::Error),

View File

@@ -1,7 +1,7 @@
use crate::check::CheckListAndVersion;
use crate::nats_types::CheckSubmissionRequest;
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Check(crate::check::Error),

View File

@@ -1,7 +1,7 @@
use crate::download::download_asset_version;
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID};
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
CreatorTypeMustBeUser,
@@ -17,11 +17,11 @@ impl std::fmt::Display for Error{
}
impl std::error::Error for Error{}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
pub struct CreateRequest{
pub ModelID:u64,
}
#[expect(nonstandard_style)]
#[allow(nonstandard_style)]
pub struct CreateResult{
pub AssetOwner:u64,
pub DisplayName:Option<String>,

View File

@@ -1,7 +1,7 @@
use crate::nats_types::CreateMapfixRequest;
use crate::create::CreateRequest;
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Create(crate::create::Error),

View File

@@ -2,7 +2,7 @@ use crate::nats_types::CreateSubmissionRequest;
use crate::create::CreateRequest;
use crate::rbx_util::GameID;
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
Create(crate::create::Error),

View File

@@ -1,4 +1,4 @@
#[expect(dead_code)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
ModelLocationDownload(rbx_asset::cloud::GetError),

View File

@@ -18,9 +18,6 @@ impl Service{
endpoint!(set_status_submitted,SubmittedRequest,NullResponse);
endpoint!(set_status_request_changes,MapfixId,NullResponse);
endpoint!(set_status_validated,MapfixId,NullResponse);
endpoint!(set_status_not_validated,MapfixId,NullResponse);
endpoint!(set_status_failed,MapfixId,NullResponse);
endpoint!(set_status_uploaded,MapfixId,NullResponse);
endpoint!(set_status_not_uploaded,MapfixId,NullResponse);
endpoint!(set_status_released,MapfixReleaseRequest,NullResponse);
endpoint!(set_status_not_released,MapfixId,NullResponse);
}

View File

@@ -11,6 +11,5 @@ impl Service{
)->Self{
Self{client}
}
endpoint!(success,OperationSuccessRequest,NullResponse);
endpoint!(fail,OperationFailRequest,NullResponse);
}

View File

@@ -18,8 +18,6 @@ impl Service{
endpoint!(set_status_submitted,SubmittedRequest,NullResponse);
endpoint!(set_status_request_changes,SubmissionId,NullResponse);
endpoint!(set_status_validated,SubmissionId,NullResponse);
endpoint!(set_status_not_validated,SubmissionId,NullResponse);
endpoint!(set_status_failed,SubmissionId,NullResponse);
endpoint!(set_status_uploaded,StatusUploadedRequest,NullResponse);
endpoint!(set_status_not_uploaded,SubmissionId,NullResponse);
endpoint!(set_status_released,SubmissionReleaseRequest,NullResponse);
}

Some files were not shown because too many files have changed in this diff Show More