require("dotenv").config() const colors = require("colors") const express = require("express") const Discord = require("discord.js") const discord = new Discord.Client({ intents: [] }) const { REST, Routes } = require('discord.js'); const { title, send } = require("process"); const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); 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); // if blacklist doesnt exist, make the file if (!fs.existsSync("./blacklist.json")) { fs.writeFileSync("./blacklist.json", "[]"); } const bulk = async (assetIds, inputCookie) => { // Impliment a rate limit of 25 requests per minute if (!assetIds || !Array.isArray(assetIds)) { return { error: "Invalid data format", status: "error" }; } // Build the batch request body const batchRequests = assetIds.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 = inputCookie || 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 = assetIds.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; }, {}); return { type: "batching-response", data: responses }; } catch (error) { return { type: "batching-response", data: assetIds.reduce((acc, item) => { acc[item] = { status: "failure", url: "", additional: "Request failed" }; return acc; }, {}) }; } } app.use((req, res, next) => { // Blacklist handler const blacklist = JSON.parse(fs.readFileSync("./blacklist.json", "utf-8")); if (blacklist.includes(req.ip)) { console.log(`[${new Date().toLocaleString()}] ${req.ip} ${req.method} ${req.url} - Blacklisted`); return res.status(403).json({ code: 403, message: codes[403].message, additional: codes[403].description }); } console.log(`[${new Date().toLocaleString()}] ${req.ip} ${req.method} ${req.url}`); 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; }, {}) }); } }); discord.on("ready", async () => { console.log(`${colors.cyan("[Discord]")} Logged in as ${discord.user.displayName}`); const commands = require("./commands.json"); //Register commands await (async () => { try { console.log(`${colors.cyan("[Discord]")} Registering Commands...`) //Global await rest.put(Routes.applicationCommands(discord.user.id), { body: commands }) console.log(`${colors.cyan("[Discord]")} Successfully registered commands.`); } catch (error) { console.error(error); } })(); app.listen(port, () => { console.log(`${colors.cyan("[EXPRESS]")} Listening on ${port}`); }); }) discord.on("interactionCreate", async (interaction) => { // When someone runs the /getaudio command, respond with a modal asking for the asset id and optional cookie switch (interaction.type) { case Discord.InteractionType.ApplicationCommand: await interaction.showModal({ //type: Discord.ComponentType.ActionRow, title: "Get Audio", custom_id: "getaudio", components: [ { type: Discord.ComponentType.ActionRow, components: [ { label: "Asset ID", custom_id: "assetId", type: Discord.ComponentType.TextInput, style: Discord.TextInputStyle.Short, required: true, placeholder: "Asset ID", maxLength: 15 } ] }, { type: Discord.ComponentType.ActionRow, components: [ { label: "Optional ROBLOSECURITY", custom_id: "cookie", type: Discord.ComponentType.TextInput, style: Discord.TextInputStyle.Short, required: false, placeholder: "ROBLOSECURITY cookie", maxLength: 100 } ] } ] }) break; case Discord.InteractionType.ModalSubmit: assetId = interaction.fields.getTextInputValue("assetId"); cookie = interaction.fields.getTextInputValue("cookie"); bulk([new String(new Number(assetId))], cookie ? cookie : null).then((data) => { if (data.data[assetId].status == "failure") { return interaction.reply({ embeds: [{ title: "Error", color: 0xff0000, description: `${data.data[assetId].message} - ${data.data[assetId].additional}` }] }); } else { return interaction.reply({ files: [{ attachment: data.data[assetId].url, name: `${assetId}.ogg` }], embeds: [{ title: "Success", color: 0x00ff00, description: `Downloaded audio file for asset ${assetId}` }] }) } }); break; } }); discord.login(process.env.DISCORD_TOKEN)