// ==UserScript==
// @name AnnaUploader (Roblox Multi-File Uploader)
// @namespace https://www.guilded.gg/u/AnnaBlox
// @version 3.5
// @description Upload multiple T-Shirts/Decals easily with AnnaUploader
// @match https://create.roblox.com/*
// @grant GM_getValue
// @grant GM_setValue
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const ROBLOX_UPLOAD_URL = "https://apis.roblox.com/assets/user-auth/v1/assets";
const ASSET_TYPE_TSHIRT = 11;
const ASSET_TYPE_DECAL = 13;
const UPLOAD_RETRY_DELAY = 2000;
const MAX_RETRIES = 3;
// ========== PERSISTENT USER CONFIG ========== //
// Will default to this value only once, then store whatever you enter
let USER_ID = GM_getValue('userId', null);
// ============================================ //
let uploadQueue = [];
let isUploading = false;
let csrfToken = null;
async function fetchCSRFToken() {
try {
const response = await fetch(ROBLOX_UPLOAD_URL, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (response.status === 403) {
const token = response.headers.get('x-csrf-token');
if (token) {
console.log('[CSRF] Token fetched:', token);
csrfToken = token;
return token;
}
}
throw new Error('Failed to fetch CSRF token');
} catch (error) {
console.error('[CSRF] Fetch error:', error);
throw error;
}
}
async function uploadFile(file, assetType, retries = 0) {
if (!csrfToken) {
await fetchCSRFToken();
}
const formData = new FormData();
formData.append("fileContent", file, file.name);
formData.append("request", JSON.stringify({
displayName: file.name.split('.')[0],
description: "Uploaded Using AnnaUploader",
assetType: assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal",
creationContext: {
creator: { userId: USER_ID },
expectedPrice: 0
}
}));
try {
const response = await fetch(ROBLOX_UPLOAD_URL, {
method: "POST",
credentials: "include",
headers: { "x-csrf-token": csrfToken },
body: formData
});
if (response.ok) {
console.log(`✅ Uploaded (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${file.name}`);
} else {
const responseText = await response.text();
console.error(`❌ Upload failed for ${file.name}: [${response.status}]`, responseText);
if (response.status === 403 && retries < MAX_RETRIES) {
console.warn(`Fetching new CSRF and retrying ${file.name}...`);
csrfToken = null;
await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
await uploadFile(file, assetType, retries + 1);
} else {
throw new Error(`Failed to upload after ${retries} retries.`);
}
}
} catch (error) {
console.error(`Upload error for ${file.name}:`, error);
throw error;
}
}
async function processUploadQueue() {
if (isUploading || uploadQueue.length === 0) return;
isUploading = true;
const { file, assetType } = uploadQueue.shift();
try {
await uploadFile(file, assetType);
} catch (error) {
console.error('Queue error:', error);
} finally {
isUploading = false;
processUploadQueue();
}
}
function handleFileSelect(files, assetType, uploadBoth = false) {
if (!files || files.length === 0) {
console.warn('No files selected.');
return;
}
for (let i = 0; i < files.length; i++) {
if (uploadBoth) {
uploadQueue.push({ file: files[i], assetType: ASSET_TYPE_TSHIRT });
uploadQueue.push({ file: files[i], assetType: ASSET_TYPE_DECAL });
console.log(`Queued (Both): ${files[i].name}`);
} else {
uploadQueue.push({ file: files[i], assetType });
console.log(`Queued (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${files[i].name}`);
}
}
processUploadQueue();
}
function createUploaderUI() {
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '10px';
container.style.right = '10px';
container.style.backgroundColor = '#fff';
container.style.border = '2px solid #000';
container.style.padding = '15px';
container.style.zIndex = '10000';
container.style.borderRadius = '8px';
container.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '10px';
container.style.fontFamily = 'Arial, sans-serif';
const title = document.createElement('h3');
title.textContent = 'Multi-File Uploader';
title.style.margin = '0';
title.style.fontSize = '16px';
container.appendChild(title);
// Upload buttons
const uploadTShirtBtn = document.createElement('button');
uploadTShirtBtn.textContent = 'Upload T-Shirts';
uploadTShirtBtn.style.padding = '8px';
uploadTShirtBtn.style.cursor = 'pointer';
uploadTShirtBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = true;
input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT));
input.click();
});
const uploadDecalBtn = document.createElement('button');
uploadDecalBtn.textContent = 'Upload Decals';
uploadDecalBtn.style.padding = '8px';
uploadDecalBtn.style.cursor = 'pointer';
uploadDecalBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = true;
input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL));
input.click();
});
const uploadBothBtn = document.createElement('button');
uploadBothBtn.textContent = 'Upload Both';
uploadBothBtn.style.padding = '8px';
uploadBothBtn.style.cursor = 'pointer';
uploadBothBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = true;
input.addEventListener('change', e => handleFileSelect(e.target.files, null, true));
input.click();
});
// Change ID button
const changeIdBtn = document.createElement('button');
changeIdBtn.textContent = 'Change ID';
changeIdBtn.style.padding = '8px';
changeIdBtn.style.cursor = 'pointer';
changeIdBtn.addEventListener('click', () => {
const newId = prompt("Enter your Roblox User ID:", USER_ID);
if (newId && !isNaN(newId)) {
USER_ID = Number(newId);
GM_setValue('userId', USER_ID);
alert(`User ID updated to ${USER_ID}`);
} else {
alert("Invalid ID. Please enter a numeric value.");
}
});
const pasteHint = document.createElement('div');
pasteHint.textContent = 'Paste images (Ctrl+V) to upload as decals!';
pasteHint.style.fontSize = '12px';
pasteHint.style.color = '#555';
// Append everything
container.appendChild(uploadTShirtBtn);
container.appendChild(uploadDecalBtn);
container.appendChild(uploadBothBtn);
container.appendChild(changeIdBtn);
container.appendChild(pasteHint);
document.body.appendChild(container);
}
function handlePaste(event) {
const items = event.clipboardData?.items;
if (!items) return;
let blob = null;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') === 0) {
blob = items[i].getAsFile();
break;
}
}
if (blob) {
event.preventDefault();
const now = new Date();
const filename = `pasted_image_${now.toISOString().replace(/[^a-z0-9]/gi, '_')}.png`;
const file = new File([blob], filename, { type: blob.type });
handleFileSelect([file], ASSET_TYPE_DECAL);
}
}
function init() {
createUploaderUI();
document.addEventListener('paste', handlePaste);
console.log('[Uploader] Initialized with User ID:', USER_ID);
}
window.addEventListener('load', init);
})();