// ==UserScript== // @name AnnaUploader (Roblox Multi-File Uploader) // @namespace https://www.guilded.gg/u/AnnaBlox // @version 4.9 // @description allows you to upload multiple T-Shirts/Decals easily with AnnaUploader // @match https://create.roblox.com/* // @match https://www.roblox.com/users/*/profile* // @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 // @downloadURL none // ==/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 = 150; const FORCED_NAME = "Uploaded Using AnnaUploader"; // Stored settings let USER_ID = GM_getValue('userId', null); let useForcedName = false; // toggle: false => use file names, true => use FORCED_NAME let useMakeUnique = false; // toggle: false => upload as-is, true => tweak a random pixel for uniqueness // Mass-upload state let massMode = false; let massQueue = []; let csrfToken = null; let batchTotal = 0; let completed = 0; let statusEl = null; let toggleBtn = null; let startBtn = null; // Fetch CSRF token from Roblox async function fetchCSRFToken() { const resp = await fetch(ROBLOX_UPLOAD_URL, { method: 'POST', credentials: 'include', headers: {'Content-Type':'application/json'}, body: JSON.stringify({}) }); if (resp.status === 403) { const tok = resp.headers.get('x-csrf-token'); if (tok) { csrfToken = tok; console.log('[CSRF] fetched:', tok); return tok; } } throw new Error('Cannot fetch CSRF token'); } // Modify one random pixel to a random color to force uniqueness function makeUniqueFile(file) { return new Promise(resolve => { const img = new Image(); img.onload = function() { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const x = Math.floor(Math.random() * canvas.width); const y = Math.floor(Math.random() * canvas.height); const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); ctx.fillStyle = `rgba(${r},${g},${b},1)`; ctx.fillRect(x, y, 1, 1); canvas.toBlob(blob => { const newFile = new File([blob], file.name, { type: file.type }); resolve(newFile); }, file.type); }; img.src = URL.createObjectURL(file); }); } // Core upload logic with retry & forced-name handling async function uploadFile(file, assetType, retries = 0, forceName = false) { if (!csrfToken) await fetchCSRFToken(); const displayName = forceName ? FORCED_NAME : file.name.split('.')[0]; const fd = new FormData(); fd.append('fileContent', file, file.name); fd.append('request', JSON.stringify({ displayName, description: FORCED_NAME, assetType: assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal", creationContext: { creator: { userId: USER_ID }, expectedPrice: 0 } })); let resp; try { resp = await fetch(ROBLOX_UPLOAD_URL, { method: 'POST', credentials: 'include', headers: { 'x-csrf-token': csrfToken }, body: fd }); if (resp.ok) { console.log(`✅ ${displayName}`); completed++; updateStatus(); return; } const txt = await resp.text(); let json; try { json = JSON.parse(txt); } catch{} const badName = resp.status === 400 && json?.message?.includes('moderated'); if (badName && retries < MAX_RETRIES && !forceName) { await new Promise(r => setTimeout(r, UPLOAD_RETRY_DELAY)); return uploadFile(file, assetType, retries + 1, true); } if (resp.status === 403 && retries < MAX_RETRIES) { csrfToken = null; await new Promise(r => setTimeout(r, UPLOAD_RETRY_DELAY)); return uploadFile(file, assetType, retries + 1, forceName); } console.error(`❌ ${file.name}: [${resp.status}]`, txt); } catch (e) { console.error('Upload error', e); } finally { if (!resp?.ok) { completed++; updateStatus(); } } } // Update on-screen status function updateStatus() { if (!statusEl) return; if (batchTotal > 0) { statusEl.textContent = `${completed} of ${batchTotal} processed`; } else { statusEl.textContent = massMode ? `${massQueue.length} items queued` : ''; } } // Handle file selection / queuing / immediate upload async function handleFileSelect(files, assetType, both = false) { if (!files || files.length === 0) return; // Queuing for mass-upload if (massMode) { for (let f of files) { const toUse = useMakeUnique ? await makeUniqueFile(f) : f; if (both) { massQueue.push({ f: toUse, type: ASSET_TYPE_TSHIRT }); massQueue.push({ f: toUse, type: ASSET_TYPE_DECAL }); } else { massQueue.push({ f: toUse, type: assetType }); } } updateStatus(); return; } // Immediate parallel upload const tasks = []; batchTotal = both ? files.length * 2 : files.length; completed = 0; updateStatus(); for (let f of files) { const toUse = useMakeUnique ? await makeUniqueFile(f) : f; if (both) { tasks.push(uploadFile(toUse, ASSET_TYPE_TSHIRT, 0, useForcedName)); tasks.push(uploadFile(toUse, ASSET_TYPE_DECAL, 0, useForcedName)); } else { tasks.push(uploadFile(toUse, assetType, 0, useForcedName)); } } Promise.all(tasks).then(() => console.log('[Uploader] done')); } // Process the queued mass-upload items function startMassUpload() { if (massQueue.length === 0) return alert('Nothing queued!'); batchTotal = massQueue.length; completed = 0; updateStatus(); const tasks = massQueue.map(item => uploadFile(item.f, item.type, 0, useForcedName) ); massQueue = []; updateStatus(); Promise.all(tasks).then(() => { alert('Mass upload complete!'); toggleBtn.textContent = 'Enable Mass Upload'; massMode = false; startBtn.style.display = 'none'; }); } // Build the little UI panel function createUploaderUI() { const c = document.createElement('div'); Object.assign(c.style, { position:'fixed', top:'10px', right:'10px', background:'#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:'8px', fontFamily:'Arial', width:'260px' }); // Close const close = document.createElement('button'); close.textContent = '×'; Object.assign(close.style, { position:'absolute', top:'5px', right:'8px', background:'transparent', border:'none', fontSize:'16px', cursor:'pointer' }); close.title = 'Close'; close.onclick = () => c.remove(); c.appendChild(close); // Title const title = document.createElement('h3'); title.textContent = 'AnnaUploader'; title.style.margin = '0 0 5px 0'; title.style.fontSize = '16px'; c.appendChild(title); // Button factory const makeBtn = (txt, fn) => { const b = document.createElement('button'); b.textContent = txt; Object.assign(b.style, { padding:'8px', cursor:'pointer' }); b.onclick = fn; return b; }; // Upload buttons c.appendChild(makeBtn('Upload T-Shirts', () => { const inp = document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.multiple=true; inp.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT); inp.click(); })); c.appendChild(makeBtn('Upload Decals', () => { const inp = document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.multiple=true; inp.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL); inp.click(); })); c.appendChild(makeBtn('Upload Both', () => { const inp = document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.multiple=true; inp.onchange = e => handleFileSelect(e.target.files, null, true); inp.click(); })); // Mass-upload toggle toggleBtn = makeBtn('Enable Mass Upload', () => { massMode = !massMode; toggleBtn.textContent = massMode ? 'Disable Mass Upload' : 'Enable Mass Upload'; startBtn.style.display = massMode ? 'block' : 'none'; massQueue = []; batchTotal = 0; completed = 0; updateStatus(); }); c.appendChild(toggleBtn); // Start mass-upload startBtn = makeBtn('Start Mass Upload', startMassUpload); startBtn.style.display = 'none'; c.appendChild(startBtn); // Forced-name toggle const nameToggleBtn = makeBtn(`Use default Name: Off`, () => { useForcedName = !useForcedName; nameToggleBtn.textContent = `Use default Name: ${useForcedName ? 'On' : 'Off'}`; }); c.appendChild(nameToggleBtn); // Make-unique toggle const uniqueToggleBtn = makeBtn(`Slip Mode: Off`, () => { useMakeUnique = !useMakeUnique; uniqueToggleBtn.textContent = `Slip Mode: ${useMakeUnique ? 'On' : 'Off'}`; }); c.appendChild(uniqueToggleBtn); // Change user ID c.appendChild(makeBtn('Change ID', () => { const inp = prompt("Enter your Roblox User ID or Profile URL:", USER_ID || ''); if (!inp) return; const m = inp.match(/users\/(\d+)/); const id = m ? m[1] : inp.trim(); if (!isNaN(id)) { USER_ID = Number(id); GM_setValue('userId', USER_ID); alert(`User ID set to ${USER_ID}`); } else alert('Invalid input.'); })); // Use profile shortcut const pm = window.location.pathname.match(/^\/users\/(\d+)\/profile/); if (pm) { c.appendChild(makeBtn('Use This Profile as ID', () => { USER_ID = Number(pm[1]); GM_setValue('userId', USER_ID); alert(`User ID set to ${USER_ID}`); })); } // Paste hint & status const hint = document.createElement('div'); hint.textContent = 'Paste images (Ctrl+V) to queue/upload'; hint.style.fontSize = '12px'; hint.style.color = '#555'; c.appendChild(hint); statusEl = document.createElement('div'); statusEl.style.fontSize = '12px'; statusEl.style.color = '#000'; c.appendChild(statusEl); document.body.appendChild(c); } // Handle clipboard paste uploads function handlePaste(e) { const items = e.clipboardData?.items; if (!items) return; for (let it of items) { if (it.type.startsWith('image')) { e.preventDefault(); const blob = it.getAsFile(); const ts = new Date().toISOString().replace(/[^a-z0-9]/gi,'_'); let name = prompt('Name (no ext):', `pasted_${ts}`); if (name === null) return; name = name.trim() || `pasted_${ts}`; const filename = name.endsWith('.png') ? name : `${name}.png`; let t = prompt('T=T-Shirt, D=Decal, C=Cancel','D'); if (!t) return; t = t.trim().toUpperCase(); let type = null; if (t === 'T') type = ASSET_TYPE_TSHIRT; else if (t === 'D') type = ASSET_TYPE_DECAL; else return; const file = new File([blob], filename, {type: blob.type}); handleFileSelect([file], type); break; } } } // Initialize on page load window.addEventListener('load', () => { createUploaderUI(); document.addEventListener('paste', handlePaste); console.log('[AnnaUploader] initialized, massMode=', massMode, 'useForcedName=', useForcedName, 'useMakeUnique=', useMakeUnique); }); })();