AnnaUploader (Roblox Multi-File Uploader)

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

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

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