// ==UserScript==
// @name AnnaUploader (Roblox Multi-File Uploader)
// @namespace https://www.guilded.gg/u/AnnaBlox
// @version 4.4
// @description allows you to Upload multiple T-Shirts/Decals easily with AnnaUploader
// @match https://create.roblox.com/*
// @run-at document-idle
// @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 = 0;
const MAX_RETRIES = 50;
const FORCED_NAME_ON_MOD = "Uploaded Using AnnaUploader";
let USER_ID = GM_getValue('userId', null);
let uploadQueue = [];
let isUploading = false;
let csrfToken = null;
let batchTotal = 0;
let completedCount = 0;
let statusElement = 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, forceName = false) {
if (!csrfToken) {
await fetchCSRFToken();
}
const displayName = forceName
? FORCED_NAME_ON_MOD
: file.name.split('.')[0];
const formData = new FormData();
formData.append("fileContent", file, file.name);
formData.append("request", JSON.stringify({
displayName: displayName,
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}`);
return;
}
const status = response.status;
const text = await response.text();
let json;
try { json = JSON.parse(text); } catch {}
const isModeratedName = status === 400 && json?.code === "INVALID_ARGUMENT" && json?.message?.includes("fully moderated");
const isInvalidNameLength = status === 400 && json?.code === "INVALID_ARGUMENT" && json?.message?.includes("name length is invalid");
if ((isModeratedName || isInvalidNameLength) && retries < MAX_RETRIES && !forceName) {
console.warn(`⚠️ Invalid name for ${file.name}: retrying with forced name...`);
await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
return await uploadFile(file, assetType, retries + 1, true);
}
if (status === 403 && retries < MAX_RETRIES) {
console.warn(`🔄 CSRF expired for ${file.name}: fetching new token and retrying...`);
csrfToken = null;
await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
return await uploadFile(file, assetType, retries + 1, forceName);
}
console.error(`❌ Upload failed for ${file.name}: [${status}]`, text);
throw new Error(`Failed to upload ${file.name} 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);
completedCount++;
updateStatus();
} catch (e) {}
finally {
isUploading = false;
processUploadQueue();
}
}
function updateStatus() {
if (!statusElement) return;
if (batchTotal > 0) {
statusElement.textContent = `${completedCount} of ${batchTotal} files uploaded successfully`;
} else {
statusElement.textContent = '';
}
}
function handleFileSelect(files, assetType, uploadBoth = false) {
if (!files || files.length === 0) {
console.warn('No files selected.');
return;
}
batchTotal = uploadBoth ? files.length * 2 : files.length;
completedCount = 0;
updateStatus();
for (let file of files) {
if (uploadBoth) {
uploadQueue.push({ file, assetType: ASSET_TYPE_TSHIRT });
uploadQueue.push({ file, assetType: ASSET_TYPE_DECAL });
console.log(`Queued (Both): ${file.name}`);
} else {
uploadQueue.push({ file, assetType });
console.log(`Queued (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${file.name}`);
}
}
processUploadQueue();
}
function createUploaderUI() {
const container = document.createElement('div');
Object.assign(container.style, {
position: 'fixed',
top: '10px',
right: '10px',
backgroundColor: '#fff',
border: '2px solid #000',
padding: '15px',
zIndex: '10000',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
display: 'flex',
flexDirection: 'column',
gap: '10px',
fontFamily: 'Arial, sans-serif',
width: '200px'
});
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
Object.assign(closeBtn.style, {
position: 'absolute',
top: '5px',
right: '8px',
background: 'transparent',
border: 'none',
fontSize: '16px',
cursor: 'pointer',
lineHeight: '1'
});
closeBtn.title = 'Close uploader';
closeBtn.addEventListener('click', () => container.remove());
container.appendChild(closeBtn);
const title = document.createElement('h3');
title.textContent = 'AnnaUploader';
title.style.margin = '0 0 5px 0';
title.style.fontSize = '16px';
container.appendChild(title);
const makeBtn = (text, onClick) => {
const btn = document.createElement('button');
btn.textContent = text;
Object.assign(btn.style, { padding: '8px', cursor: 'pointer' });
btn.addEventListener('click', onClick);
return btn;
};
container.appendChild(makeBtn('Upload T-Shirts', () => {
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();
}));
container.appendChild(makeBtn('Upload Decals', () => {
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();
}));
container.appendChild(makeBtn('Upload Both', () => {
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();
}));
container.appendChild(makeBtn('Change ID', () => {
const input = prompt("Enter your Roblox User ID or Profile URL:", USER_ID || '');
if (!input) return;
const urlMatch = input.match(/roblox\.com\/users\/(\d+)\/profile/i);
let newId = urlMatch ? urlMatch[1] : (!isNaN(input.trim()) ? input.trim() : null);
if (newId) {
USER_ID = Number(newId);
GM_setValue('userId', USER_ID);
alert(`User ID updated to ${USER_ID}`);
} else {
alert("Invalid input. Please enter a numeric ID or a valid profile URL.");
}
}));
const pasteHint = document.createElement('div');
pasteHint.textContent = 'Paste images (Ctrl+V) to upload—name & type it first!';
pasteHint.style.fontSize = '12px';
pasteHint.style.color = '#555';
container.appendChild(pasteHint);
statusElement = document.createElement('div');
statusElement.style.fontSize = '12px';
statusElement.style.color = '#000';
statusElement.textContent = '';
container.appendChild(statusElement);
document.body.appendChild(container);
}
function handlePaste(event) {
const items = event.clipboardData?.items;
if (!items) return;
for (let item of items) {
if (item.type.indexOf('image') === 0) {
event.preventDefault();
const blob = item.getAsFile();
const now = new Date();
const defaultBase = `pasted_image_${now.toISOString().replace(/[^a-z0-9]/gi, '_')}`;
// 1) Name prompt
let nameInput = prompt("Enter a name for the pasted image (no extension):", defaultBase);
if (nameInput === null) return; // cancelled
nameInput = nameInput.trim() || defaultBase;
const filename = nameInput.endsWith('.png') ? nameInput : `${nameInput}.png`;
// 2) Type prompt
let typeInput = prompt(
"Upload as:\n T = T-Shirt\n D = Decal\n C = Cancel",
"D"
);
if (!typeInput) return;
typeInput = typeInput.trim().toUpperCase();
let chosenType = null;
if (typeInput === 'T') chosenType = ASSET_TYPE_TSHIRT;
else if (typeInput === 'D') chosenType = ASSET_TYPE_DECAL;
else return; // Cancel or invalid
const file = new File([blob], filename, { type: blob.type });
handleFileSelect([file], chosenType);
break;
}
}
}
function init() {
createUploaderUI();
document.addEventListener('paste', handlePaste);
console.log('[Uploader] Initialized with User ID:', USER_ID);
}
window.addEventListener('load', init);
})();