AnnaUploader (Roblox Multi-File Uploader)

allows you to Upload multiple T-Shirts/Decals easily with AnnaUploader

当前为 2025-05-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AnnaUploader (Roblox Multi-File Uploader)
  3. // @namespace https://www.guilded.gg/u/AnnaBlox
  4. // @version 4.1
  5. // @description allows you to Upload multiple T-Shirts/Decals easily with AnnaUploader
  6. // @match https://create.roblox.com/*
  7. // @run-at document-idle
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const ROBLOX_UPLOAD_URL = "https://apis.roblox.com/assets/user-auth/v1/assets";
  18. const ASSET_TYPE_TSHIRT = 11;
  19. const ASSET_TYPE_DECAL = 13;
  20. const UPLOAD_RETRY_DELAY = 2000;
  21. const MAX_RETRIES = 3;
  22. const FORCED_NAME_ON_MOD = "Uploaded Using AnnaUploader";
  23.  
  24. let USER_ID = GM_getValue('userId', null);
  25. let uploadQueue = [];
  26. let isUploading = false;
  27. let csrfToken = null;
  28. let batchTotal = 0;
  29. let completedCount = 0;
  30. let statusElement = null;
  31.  
  32. async function fetchCSRFToken() {
  33. try {
  34. const response = await fetch(ROBLOX_UPLOAD_URL, {
  35. method: 'POST',
  36. credentials: 'include',
  37. headers: { 'Content-Type': 'application/json' },
  38. body: JSON.stringify({})
  39. });
  40. if (response.status === 403) {
  41. const token = response.headers.get('x-csrf-token');
  42. if (token) {
  43. console.log('[CSRF] Token fetched:', token);
  44. csrfToken = token;
  45. return token;
  46. }
  47. }
  48. throw new Error('Failed to fetch CSRF token');
  49. } catch (error) {
  50. console.error('[CSRF] Fetch error:', error);
  51. throw error;
  52. }
  53. }
  54.  
  55. async function uploadFile(file, assetType, retries = 0, forceName = false) {
  56. if (!csrfToken) {
  57. await fetchCSRFToken();
  58. }
  59.  
  60. const displayName = forceName
  61. ? FORCED_NAME_ON_MOD
  62. : file.name.split('.')[0];
  63.  
  64. const formData = new FormData();
  65. formData.append("fileContent", file, file.name);
  66. formData.append("request", JSON.stringify({
  67. displayName: displayName,
  68. description: "Uploaded Using AnnaUploader",
  69. assetType: assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal",
  70. creationContext: {
  71. creator: { userId: USER_ID },
  72. expectedPrice: 0
  73. }
  74. }));
  75.  
  76. try {
  77. const response = await fetch(ROBLOX_UPLOAD_URL, {
  78. method: "POST",
  79. credentials: "include",
  80. headers: { "x-csrf-token": csrfToken },
  81. body: formData
  82. });
  83.  
  84. if (response.ok) {
  85. console.log(`✅ Uploaded (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${file.name}`);
  86. return;
  87. }
  88.  
  89. const status = response.status;
  90. const text = await response.text();
  91. let json;
  92. try { json = JSON.parse(text); } catch {}
  93.  
  94. const isModeratedName = status === 400 && json?.code === "INVALID_ARGUMENT" && json?.message?.includes("fully moderated");
  95. const isInvalidNameLength = status === 400 && json?.code === "INVALID_ARGUMENT" && json?.message?.includes("name length is invalid");
  96.  
  97. if ((isModeratedName || isInvalidNameLength) && retries < MAX_RETRIES && !forceName) {
  98. console.warn(`⚠️ Invalid name for ${file.name}: retrying with forced name...`);
  99. await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
  100. return await uploadFile(file, assetType, retries + 1, true);
  101. }
  102.  
  103. if (status === 403 && retries < MAX_RETRIES) {
  104. console.warn(`🔄 CSRF expired for ${file.name}: fetching new token and retrying...`);
  105. csrfToken = null;
  106. await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
  107. return await uploadFile(file, assetType, retries + 1, forceName);
  108. }
  109.  
  110. console.error(`❌ Upload failed for ${file.name}: [${status}]`, text);
  111. throw new Error(`Failed to upload ${file.name} after ${retries} retries.`);
  112. } catch (error) {
  113. console.error(`Upload error for ${file.name}:`, error);
  114. throw error;
  115. }
  116. }
  117.  
  118. async function processUploadQueue() {
  119. if (isUploading || uploadQueue.length === 0) return;
  120. isUploading = true;
  121. const { file, assetType } = uploadQueue.shift();
  122. try {
  123. await uploadFile(file, assetType);
  124. completedCount++;
  125. updateStatus();
  126. } catch (e) {}
  127. finally {
  128. isUploading = false;
  129. processUploadQueue();
  130. }
  131. }
  132.  
  133. function updateStatus() {
  134. if (!statusElement) return;
  135. if (batchTotal > 0) {
  136. statusElement.textContent = `${completedCount} of ${batchTotal} files uploaded successfully`;
  137. } else {
  138. statusElement.textContent = '';
  139. }
  140. }
  141.  
  142. function handleFileSelect(files, assetType, uploadBoth = false) {
  143. if (!files || files.length === 0) {
  144. console.warn('No files selected.');
  145. return;
  146. }
  147. batchTotal = uploadBoth ? files.length * 2 : files.length;
  148. completedCount = 0;
  149. updateStatus();
  150.  
  151. for (let file of files) {
  152. if (uploadBoth) {
  153. uploadQueue.push({ file, assetType: ASSET_TYPE_TSHIRT });
  154. uploadQueue.push({ file, assetType: ASSET_TYPE_DECAL });
  155. console.log(`Queued (Both): ${file.name}`);
  156. } else {
  157. uploadQueue.push({ file, assetType });
  158. console.log(`Queued (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${file.name}`);
  159. }
  160. }
  161. processUploadQueue();
  162. }
  163.  
  164. function createUploaderUI() {
  165. const container = document.createElement('div');
  166. Object.assign(container.style, {
  167. position: 'fixed',
  168. top: '10px',
  169. right: '10px',
  170. backgroundColor: '#fff',
  171. border: '2px solid #000',
  172. padding: '15px',
  173. zIndex: '10000',
  174. borderRadius: '8px',
  175. boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
  176. display: 'flex',
  177. flexDirection: 'column',
  178. gap: '10px',
  179. fontFamily: 'Arial, sans-serif',
  180. width: '200px'
  181. });
  182.  
  183. const closeBtn = document.createElement('button');
  184. closeBtn.textContent = '×';
  185. Object.assign(closeBtn.style, {
  186. position: 'absolute',
  187. top: '5px',
  188. right: '8px',
  189. background: 'transparent',
  190. border: 'none',
  191. fontSize: '16px',
  192. cursor: 'pointer',
  193. lineHeight: '1'
  194. });
  195. closeBtn.title = 'Close uploader';
  196. closeBtn.addEventListener('click', () => container.remove());
  197. container.appendChild(closeBtn);
  198.  
  199. const title = document.createElement('h3');
  200. title.textContent = 'AnnaUploader';
  201. title.style.margin = '0 0 5px 0';
  202. title.style.fontSize = '16px';
  203. container.appendChild(title);
  204.  
  205. const makeBtn = (text, onClick) => {
  206. const btn = document.createElement('button');
  207. btn.textContent = text;
  208. Object.assign(btn.style, { padding: '8px', cursor: 'pointer' });
  209. btn.addEventListener('click', onClick);
  210. return btn;
  211. };
  212.  
  213. container.appendChild(makeBtn('Upload T-Shirts', () => {
  214. const input = document.createElement('input');
  215. input.type = 'file'; input.accept = 'image/*'; input.multiple = true;
  216. input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT));
  217. input.click();
  218. }));
  219. container.appendChild(makeBtn('Upload Decals', () => {
  220. const input = document.createElement('input');
  221. input.type = 'file'; input.accept = 'image/*'; input.multiple = true;
  222. input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL));
  223. input.click();
  224. }));
  225. container.appendChild(makeBtn('Upload Both', () => {
  226. const input = document.createElement('input');
  227. input.type = 'file'; input.accept = 'image/*'; input.multiple = true;
  228. input.addEventListener('change', e => handleFileSelect(e.target.files, null, true));
  229. input.click();
  230. }));
  231. container.appendChild(makeBtn('Change ID', () => {
  232. const input = prompt("Enter your Roblox User ID or Profile URL:", USER_ID || '');
  233. if (!input) return;
  234.  
  235. // Try to extract numeric ID from profile URL
  236. const urlMatch = input.match(/roblox\.com\/users\/(\d+)\/profile/i);
  237. let newId = null;
  238. if (urlMatch) {
  239. newId = urlMatch[1];
  240. } else if (!isNaN(input.trim())) {
  241. newId = input.trim();
  242. }
  243.  
  244. if (newId) {
  245. USER_ID = Number(newId);
  246. GM_setValue('userId', USER_ID);
  247. alert(`User ID updated to ${USER_ID}`);
  248. } else {
  249. alert("Invalid input. Please enter a numeric ID or a valid profile URL.");
  250. }
  251. }));
  252.  
  253. const pasteHint = document.createElement('div');
  254. pasteHint.textContent = 'Paste images (Ctrl+V) to upload as decals!';
  255. pasteHint.style.fontSize = '12px';
  256. pasteHint.style.color = '#555';
  257. container.appendChild(pasteHint);
  258.  
  259. statusElement = document.createElement('div');
  260. statusElement.style.fontSize = '12px';
  261. statusElement.style.color = '#000';
  262. statusElement.textContent = '';
  263. container.appendChild(statusElement);
  264.  
  265. document.body.appendChild(container);
  266. }
  267.  
  268. function handlePaste(event) {
  269. const items = event.clipboardData?.items;
  270. if (!items) return;
  271. for (let item of items) {
  272. if (item.type.indexOf('image') === 0) {
  273. event.preventDefault();
  274. const blob = item.getAsFile();
  275. const now = new Date();
  276. const filename = `pasted_image_${now.toISOString().replace(/[^a-z0-9]/gi, '_')}.png`;
  277. const file = new File([blob], filename, { type: blob.type });
  278. handleFileSelect([file], ASSET_TYPE_DECAL);
  279. break;
  280. }
  281. }
  282. }
  283.  
  284. function init() {
  285. createUploaderUI();
  286. document.addEventListener('paste', handlePaste);
  287. console.log('[Uploader] Initialized with User ID:', USER_ID);
  288. }
  289.  
  290. window.addEventListener('load', init);
  291. })();