Compare commits

..

11 Commits
cli ... master

Author SHA1 Message Date
7689001e74 Merge pull request '404 / 500 Thumbnails + Fix Regex Capture Groups' (#168) from staging into master
Reviewed-on: StrafesNET/maps-service#168
2025-06-07 04:02:26 +00:00
127402fa77 Merge pull request 'fix regex capture groups' (#167) from pr2 into staging
Reviewed-on: StrafesNET/maps-service#167
2025-06-07 03:58:16 +00:00
40f83a4e30 fix regex capture groups 2025-06-06 20:52:17 -07:00
07391a84cb Merge pull request 'thumbnail fix - will this WORK THIS TIME?' (#154) from thumbnail-fix-1 into staging
Reviewed-on: StrafesNET/maps-service#154
Reviewed-by: Quaternions <quaternions@noreply@itzana.me>
2025-06-06 02:51:35 +00:00
ic3w0lf
3f848a35c8 implement cache de-exister 2025-06-05 17:42:34 -06:00
e89abed3d5 Merge pull request 'Thumbnail Fixes + Bypass Submit Button' (#161) from staging into master
Reviewed-on: StrafesNET/maps-service#161
2025-06-05 01:34:35 +00:00
ic3w0lf
8d5bd9e523 Fix error & include error message in response headers 2025-06-03 20:52:43 -06:00
ic3w0lf
e1fc637619 Implement errorImageResponse 2025-06-03 20:42:37 -06:00
ic3w0lf
762ee874a0 thumbnail fix - will this WORK THIS TIME? 2025-06-03 20:03:09 -06:00
b792d33164 Merge pull request 'Update Rust Dependencies (Roblox Format Zstd Support)' (#142) from staging into master
Reviewed-on: StrafesNET/maps-service#142
2025-06-01 23:13:58 +00:00
929b5949f0 Merge pull request 'Snapshot "Working" Code' (#139) from staging into master
Reviewed-on: StrafesNET/maps-service#139
2025-04-27 21:21:05 +00:00
12 changed files with 98 additions and 264 deletions

129
Cargo.lock generated
View File

@@ -54,56 +54,6 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
]
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -284,61 +234,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "clap"
version = "4.5.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cli"
version = "0.1.0"
dependencies = [
"clap",
"maps-validation",
"rbx_binary",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -987,12 +882,6 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
@@ -1234,12 +1123,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "openssl"
version = "0.10.72"
@@ -2004,12 +1887,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "submissions-api"
version = "0.7.1"
@@ -2364,12 +2241,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"

View File

@@ -2,6 +2,5 @@
members = [
"validation",
"validation/api",
"validation/cli",
]
resolver = "2"

View File

@@ -1,9 +0,0 @@
[package]
name = "cli"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.39", features = ["derive"] }
maps-validation = { path = ".." }
rbx_binary = "1.0.0"

View File

@@ -1,48 +0,0 @@
use clap::{Args,Parser,Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(author,version,about,long_about=None)]
#[command(propagate_version=true)]
struct Cli{
#[command(subcommand)]
command:Commands,
}
#[derive(Subcommand)]
enum Commands{
Check(CheckCommand),
}
#[derive(Args)]
struct CheckCommand{
#[arg(long)]
file:PathBuf,
}
fn main(){
let cli=Cli::parse();
match cli.command{
Commands::Check(command)=>command.run().unwrap(),
}
}
#[allow(dead_code)]
#[derive(Debug)]
enum CheckError{
Io(std::io::Error),
Binary(rbx_binary::DecodeError),
}
impl CheckCommand{
fn run(self)->Result<(),CheckError>{
let file=std::fs::read(self.file).map_err(CheckError::Io)?;
let dom=rbx_binary::from_reader(file.as_slice()).map_err(CheckError::Binary)?;
let status=maps_validation::message_handler::MessageHandler::check_dom(&dom).unwrap();
dbg!(status);
Ok(())
}
}

View File

@@ -73,26 +73,26 @@ 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 bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$");
if let Some(captures)=bonus_start_pattern.captures(other){
return Ok(Self{
zone:Zone::Start,
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
}
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$");
if let Some(captures)=bonus_finish_pattern.captures(other){
return Ok(Self{
zone:Zone::Finish,
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
}
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
if let Some(captures)=bonus_finish_pattern.captures(other){
return Ok(Self{
zone:Zone::Anticheat,
mode_id:ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?),
});
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{
zone:Zone::Start,
mode_id:ModeID(mode_id.as_str().parse().map_err(IDParseError::ParseInt)?),
});
}
if let Some(mode_id)=captures.get(3).or(captures.get(4)){
return Ok(Self{
zone:Zone::Finish,
mode_id:ModeID(mode_id.as_str().parse().map_err(IDParseError::ParseInt)?),
});
}
if let Some(mode_id)=captures.get(5).or(captures.get(6)){
return Ok(Self{
zone:Zone::Anticheat,
mode_id:ModeID(mode_id.as_str().parse().map_err(IDParseError::ParseInt)?),
});
}
}
Err(IDParseError::NoCaptures)
}
@@ -227,7 +227,6 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
let mut counts=Counts::default();
for instance in dom.descendants_of(model_instance.referent()){
if class_is_a(instance.class.as_str(),"BasePart"){
println!("{}",instance.name);
// Zones
match instance.name.parse(){
Ok(ModeElement{zone:Zone::Start,mode_id})=>counts.mode_start_counts.entry(mode_id).or_default().push(instance.name.as_str()),
@@ -372,7 +371,6 @@ impl<ID:Copy+Eq+std::hash::Hash,T> SetDifferenceCheckContextAtLeastOne<ID,T>{
}
/// Info lifted out of a fully compliant map
#[derive(Debug)]
pub struct MapInfoOwned{
pub display_name:String,
pub creator:String,
@@ -793,7 +791,6 @@ impl MapCheckList{
}
}
#[derive(Debug)]
pub struct Summary{
pub summary:String,
pub json:serde_json::Value,
@@ -827,11 +824,6 @@ impl crate::message_handler::MessageHandler{
// decode dom (slow!)
let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?;
let status=Self::check_dom(&dom)?;
Ok(CheckReportAndVersion{status,version})
}
pub fn check_dom(dom:&rbx_dom_weak::WeakDom)->Result<Result<MapInfoOwned, Summary>,Error>{
// extract the root instance
let model_instance=get_root_instance(&dom).map_err(Error::GetRootInstance)?;
@@ -851,6 +843,6 @@ impl crate::message_handler::MessageHandler{
Err(Err(e))=>return Err(Error::ToJsonValue(e)),
};
Ok(status)
Ok(CheckReportAndVersion{status,version})
}
}

View File

@@ -1,16 +0,0 @@
pub mod rbx_util;
pub mod message_handler;
pub mod nats_types;
pub mod types;
pub mod download;
pub mod check;
pub mod check_mapfix;
pub mod check_submission;
pub mod create;
pub mod create_mapfix;
pub mod create_submission;
pub mod upload_mapfix;
pub mod upload_submission;
pub mod validator;
pub mod validate_mapfix;
pub mod validate_submission;

View File

@@ -40,7 +40,6 @@ fn find_first_child_name_and_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&r
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name&&inst.class==class)
}
#[derive(Debug)]
pub enum GameID{
Bhop=1,
Surf=2,

BIN
web/public/errors/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
web/public/errors/500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,35 @@
import path from 'path';
import { promises as fs } from 'fs';
import { NextResponse } from 'next/server';
export async function errorImageResponse(
statusCode: number = 500,
options?: { message?: string }
): Promise<NextResponse> {
const file = `${statusCode}.png`;
const filePath = path.join(process.cwd(), 'public/errors', file);
const headers: Record<string, string> = {
'Content-Type': 'image/png',
};
if (options?.message) {
headers['X-Error-Message'] = encodeURIComponent(options.message);
}
try {
const buffer = await fs.readFile(filePath);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: statusCode,
headers,
});
} catch {
const fallback = path.join(process.cwd(), 'public/errors', '500.png');
const buffer = await fs.readFile(fallback);
headers['Content-Length'] = buffer.length.toString();
return new NextResponse(buffer, {
status: 500,
headers,
});
}
}

View File

@@ -1,8 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';
import { errorImageResponse } from '@/app/lib/errorImageResponse';
const cache = new Map<number, { buffer: Buffer; expires: number }>();
const CACHE_TTL = 15 * 60 * 1000;
setInterval(() => {
const now = Date.now();
for (const [key, value] of cache.entries()) {
if (value.expires <= now) {
cache.delete(key);
}
}
}, 60 * 5 * 1000);
export async function GET(
request: NextRequest,
context: { params: Promise<{ assetId: number }> }
@@ -10,10 +20,9 @@ export async function GET(
const { assetId } = await context.params;
if (!assetId) {
return NextResponse.json(
{ error: 'Missing asset ID' },
{ status: 400 }
);
return errorImageResponse(400, {
message: "Missing asset ID",
})
}
let finalAssetId = assetId;
@@ -31,17 +40,17 @@ export async function GET(
} catch { }
const now = Date.now();
const cached = cache.get(finalAssetId);
const cached = cache.get(finalAssetId);
if (cached && cached.expires > now) {
return new NextResponse(cached.buffer, {
headers: {
'Content-Type': 'image/png',
'Content-Length': cached.buffer.length.toString(),
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
}
if (cached && cached.expires > now) {
return new NextResponse(cached.buffer, {
headers: {
'Content-Type': 'image/png',
'Content-Length': cached.buffer.length.toString(),
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
}
try {
const response = await fetch(
@@ -49,22 +58,21 @@ export async function GET(
);
if (!response.ok) {
throw new Error('Failed to fetch thumbnail JSON');
throw new Error(`Failed to fetch thumbnail JSON [${response.status}]`)
}
const data = await response.json();
const imageUrl = data.data[0]?.imageUrl;
if (!imageUrl) {
return NextResponse.json(
{ error: 'No image URL found in the response' },
{ status: 404 }
);
return errorImageResponse(404, {
message: "No image URL found in the response",
})
}
const imageResponse = await fetch(imageUrl);
if (!imageResponse.ok) {
throw new Error('Failed to fetch the image');
throw new Error(`Failed to fetch the image [${imageResponse.status}]`)
}
const arrayBuffer = await imageResponse.arrayBuffer();
@@ -79,10 +87,9 @@ export async function GET(
'Cache-Control': `public, max-age=${CACHE_TTL / 1000}`,
},
});
} catch {
return NextResponse.json(
{ error: 'Failed to fetch or process thumbnail' },
{ status: 500 }
);
} catch (err) {
return errorImageResponse(500, {
message: `Failed to fetch or process thumbnail: ${err}`,
})
}
}

View File

@@ -1,16 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server"
export async function GET(
request: NextRequest,
context: { params: Promise<{ mapId: string }> }
): Promise<NextResponse> {
// TODO: implement this, we need a cdn for in-game map thumbnails...
if (!process.env.API_HOST) {
throw new Error('env variable "API_HOST" is not set')
}
const { mapId } = await context.params;
const { mapId } = await context.params
const protocol = request.headers.get("x-forwarded-proto") || "https";
const host = request.headers.get("host");
const origin = `${protocol}://${host}`;
const apiHost = process.env.API_HOST.replace(/\/api\/?$/, "")
const redirectPath = `/thumbnails/asset/${mapId}`
const redirectUrl = `${apiHost}${redirectPath}`
return NextResponse.redirect(`${origin}/thumbnails/asset/${mapId}`);
return NextResponse.redirect(redirectUrl)
}