require("dotenv").config() const express = require("express") const codes = require("./codes.json"); const app = express() const fs = require("fs"); const port = process.env.SERVER_PORT || 3000; app.use(express.json()); app.set("trust proxy", 1); app.use((req, res, next) => { console.log(`[${new Date().toLocaleString()}] ${req.ip} ${req.method} ${req.url}`); next(); }) // if blacklist doesnt exist, make the file if (!fs.existsSync("./blacklist.json")) { fs.writeFileSync("./blacklist.json", "[]"); } app.use((req, res, next) => { // Blacklist handler let blacklist = require("./blacklist.json"); // Get it every time to update it if (blacklist.includes(req.ip)) { return res.status(403).json({ code: 403, message: codes[403].message, additional: codes[403].description }); } next(); }) var rateLimits = {}; app.get("/", (req, res) => { // load index res.sendFile(__dirname + "/static/index.html") }); app.post("/", async (req, res) => { // Impliment a rate limit of 25 requests per minute const ip = req.ip; console.log(rateLimits[ip]) if (!rateLimits[ip]) { rateLimits[ip] = { count: 0, lastRequest: Date.now() }; } const limit = rateLimits[ip]; const now = Date.now(); if (now - limit.lastRequest > 60000) { limit.count = 0; limit.lastRequest = now; } if (limit.count >= 25) { return res.status(429).json({ code: 429, error: "Rate limit exceeded", status: "error" }); } limit.count++; // Validate request body if (!req.body.type || req.body.type !== "batching") { return res.status(400).json({ error: "Invalid request type", status: "error" }); } // Check that the body is valid json, if not throw an error if (!req.body.data || !Array.isArray(req.body.data)) { return res.status(400).json({ error: "Invalid data format", status: "error" }); } // Build the batch request body const batchRequests = req.body.data.map(item => { const assetId = item; if (!assetId) { return { assetId, requestId: assetId, status: "failure", additional: "Missing assetId." }; } return { assetId, requestId: assetId }; }); // If body.cookie is provided, use it, else use process.env.COOKIE const cookie = req.body.cookie || process.env.COOKIE; const options = { method: 'POST', headers: { authority: 'assetdelivery.roblox.com', accept: '', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', 'content-type': 'application/json', origin: 'https://create.roblox.com', pragma: 'no-cache', referer: 'https://create.roblox.com/', 'roblox-browser-asset-request': 'true', 'roblox-place-id': '0', // Use a default or placeholder value if needed 'sec-ch-ua': '"Opera GX";v="105", "Chromium";v="119", "Not?A_Brand";v="24"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-site', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0', Cookie: `.ROBLOSECURITY=${cookie};` }, body: JSON.stringify(batchRequests) }; try { let response = await fetch("https://assetdelivery.roblox.com/v1/assets/batch", options); let json = await response.json(); //console.log(JSON.stringify(json, null, 2)); // Build the response object const responses = req.body.data.reduce((acc, item, index) => { const assetId = item; if (json[index].errors) { const errorCode = json[index].errors[0].code; acc[assetId] = { status: "failure", code: errorCode, message: codes[errorCode].message, additional: codes[errorCode].description }; } else if (json[index].assetTypeId !== 3) { // Return 415, the asset isnt audio acc[assetId] = { status: "failure", code: 415, message: codes[415].message, additional: codes[415].description }; } else { acc[assetId] = { status: "success", url: json[index].location }; } return acc; }, {}); res.json({ type: "batching-response", data: responses }); } catch (error) { res.status(500).json({ type: "batching-response", data: req.body.data.reduce((acc, item) => { acc[item] = { status: "failure", url: "", additional: "Request failed" }; return acc; }, {}) }); } }); app.listen(port, () => { console.log(`Listening on ${port}`); });