FR:Reborn - Agents extension

Upload QCs from your favorite agent to Imgur + QC server

安装此脚本?
作者推荐脚本

您可能也喜欢FR:Reborn

安装此脚本
  1. // ==UserScript==
  2. // @name FR:Reborn - Agents extension
  3. // @namespace https://www.reddit.com/user/RobotOilInc
  4. // @version 2.3.4
  5. // @description Upload QCs from your favorite agent to Imgur + QC server
  6. // @author RobotOilInc
  7. // @match https://www.basetao.com/*my_account/order/*
  8. // @match https://basetao.com/*my_account/order/*
  9. // @match https://www.cssbuy.com/*name=orderlist*
  10. // @match https://cssbuy.com/*name=orderlist*
  11. // @match https://superbuy.com/order*
  12. // @match https://www.superbuy.com/order*
  13. // @match https://wegobuy.com/order*
  14. // @match https://www.wegobuy.com/order*
  15. // @grant GM_addStyle
  16. // @grant GM_getResourceText
  17. // @grant GM_getValue
  18. // @grant GM_setValue
  19. // @grant GM_openInTab
  20. // @grant GM_registerMenuCommand
  21. // @license MIT
  22. // @homepageURL https://www.fashionreps.page/
  23. // @supportURL https://gf.qytechs.cn/en/scripts/426977-fr-reborn-agents-extension
  24. // @include https://www.basetao.com/index/orderphoto/itemimg/*
  25. // @include https://basetao.com/index/orderphoto/itemimg/*
  26. // @require https://unpkg.com/sweetalert2@11.10.6/dist/sweetalert2.js
  27. // @require https://unpkg.com/js-logger@1.6.1/src/logger.js
  28. // @require https://unpkg.com/spark-md5@3.0.2/spark-md5.js
  29. // @require https://unpkg.com/@zip.js/zip.js@2.3.15/dist/zip-full.js
  30. // @require https://unpkg.com/file-saver@2.0.5/dist/FileSaver.js
  31. // @require https://unpkg.com/jquery@3.6.0/dist/jquery.js
  32. // @require https://unpkg.com/jquery.ajax-retry@0.2.7/src/jquery.ajax-retry.js
  33. // @require https://unpkg.com/@sentry/browser@6.19.7/build/bundle.js
  34. // @require https://unpkg.com/@sentry/tracing@6.19.7/build/bundle.tracing.js
  35. // @require https://unpkg.com/swagger-client@3.26.0/dist/swagger-client.browser.js
  36. // @require https://gf.qytechs.cn/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657
  37. // @resource sweetalert2 https://unpkg.com/sweetalert2@11.0.15/dist/sweetalert2.min.css
  38. // @run-at document-end
  39. // @icon https://i.imgur.com/mYBHjAg.png
  40. // ==/UserScript==
  41.  
  42. // Define default toast
  43. const Toast = Swal.mixin({
  44. showConfirmButton: false,
  45. timerProgressBar: true,
  46. position: 'top-end',
  47. timer: 4000,
  48. toast: true,
  49. didOpen: (toast) => {
  50. toast.addEventListener('mouseenter', Swal.stopTimer);
  51. toast.addEventListener('mouseleave', Swal.resumeTimer);
  52. },
  53. });
  54.  
  55. /**
  56. * @param text {string}
  57. * @param type {null|('success'|'error'|'warning'|'info')}
  58. */
  59. const Snackbar = function (text, type = null) {
  60. Toast.fire({ title: text, icon: type != null ? type : 'info' });
  61. };
  62.  
  63. /**
  64. * @return {Promise<boolean>}
  65. */
  66. const ConfirmDialog = async function () {
  67. return new Promise((resolve) => {
  68. Swal.fire({
  69. title: 'Are you sure?',
  70. icon: 'warning',
  71. showCancelButton: true,
  72. confirmButtonColor: '#3085d6',
  73. cancelButtonColor: '#d33',
  74. confirmButtonText: 'Yes',
  75. }).then((result) => resolve(result.isConfirmed));
  76. });
  77. };
  78.  
  79. class ImgurError extends Error {
  80. /**
  81. * @param message {string}
  82. * @param previous {Error}
  83. */
  84. constructor(message, previous) {
  85. super(message);
  86. this.name = 'ImgurError';
  87. this.previous = previous;
  88. }
  89. }
  90.  
  91. class ImgurSlowdownError extends ImgurError {
  92. constructor(message, previous) {
  93. super(`Imgur is telling us to slow down:\n${message}`, previous);
  94. }
  95. }
  96.  
  97. // Possible websites
  98. const WEBSITE_1688 = '1688';
  99. const WEBSITE_TAOBAO = 'taobao';
  100. const WEBSITE_TMALL = 'tmall';
  101. const WEBSITE_YUPOO = 'yupoo';
  102. const WEBSITE_WEIDIAN = 'weidian';
  103. const WEBSITE_XIANYU = 'xianyu';
  104. const WEBSITE_UNKNOWN = 'unknown';
  105.  
  106. /**
  107. * @internal
  108. * @param url {string}
  109. * @returns {string}
  110. */
  111. const ensureNonEncodedURL = (url) => {
  112. if (url === decodeURIComponent(url || '')) {
  113. return url;
  114. }
  115.  
  116. // Grab the encoded URL
  117. const encodedURL = new URL(url).searchParams.get('url') || '';
  118. if (encodedURL.length === 0) {
  119. return url;
  120. }
  121.  
  122. // Decode said encoded URL
  123. const decodedURL = decodeURIComponent(encodedURL);
  124. if (decodedURL.length === 0) {
  125. return url;
  126. }
  127.  
  128. return decodedURL;
  129. };
  130.  
  131. /**
  132. * @param url {string}
  133. * @returns {boolean}
  134. */
  135. const isUrl = (url) => { try { return Boolean(new URL(url)); } catch (e) { return false; } };
  136.  
  137. /**
  138. * @param originalUrl {string}
  139. * @param website {string}
  140. * @returns {string}
  141. */
  142. const cleanPurchaseUrl = (originalUrl, website) => {
  143. const url = ensureNonEncodedURL(originalUrl);
  144.  
  145. const idMatches = url.match(/[?&]id=(\d+)|[?&]itemID=(\d+)|\/?[albums]\/(\d+)|offer\/(\d+)/i);
  146. const authorMatches = url.match(/https?:\/\/(.+)\.x\.yupoo\.com/);
  147.  
  148. if (website === WEBSITE_TAOBAO && idMatches[1].length !== 0) {
  149. return `https://item.taobao.com/item.htm?id=${idMatches[1]}`;
  150. }
  151.  
  152. if (website === WEBSITE_TMALL && idMatches[1].length !== 0) {
  153. return `https://detail.tmall.com/item.htm?id=${idMatches[1]}`;
  154. }
  155.  
  156. if (website === WEBSITE_XIANYU && idMatches[1].length !== 0) {
  157. return `https://2.taobao.com/item.htm?id=${idMatches[1]}`;
  158. }
  159.  
  160. if (website === WEBSITE_WEIDIAN && idMatches[2].length !== 0) {
  161. return `https://weidian.com/item.html?itemID=${idMatches[2]}`;
  162. }
  163.  
  164. if (website === WEBSITE_YUPOO && idMatches[3].length !== 0 && authorMatches[1].length !== 0) {
  165. return `https://${authorMatches[1]}.x.yupoo.com/albums/${idMatches[3]}`;
  166. }
  167.  
  168. if (website === WEBSITE_1688 && idMatches[4].length !== 0) {
  169. return `https://detail.1688.com/offer/${idMatches[4]}.html`;
  170. }
  171.  
  172. // Just return the original URL with some clean up
  173. return originalUrl.replace('http://', 'https://').replace('?uid=1', '').trim();
  174. };
  175.  
  176. /**
  177. * @param originalUrl {string}
  178. * @returns {string}
  179. */
  180. const determineWebsite = (originalUrl) => {
  181. if (originalUrl.indexOf('1688.com') !== -1) {
  182. return WEBSITE_1688;
  183. }
  184.  
  185. // Check more specific taobao first
  186. if (originalUrl.indexOf('market.m.taobao.com') !== -1 || originalUrl.indexOf('2.taobao.com') !== -1) {
  187. return WEBSITE_XIANYU;
  188. }
  189.  
  190. if (originalUrl.indexOf('taobao.com') !== -1) {
  191. return WEBSITE_TAOBAO;
  192. }
  193.  
  194. if (originalUrl.indexOf('detail.tmall.com') !== -1) {
  195. return WEBSITE_TMALL;
  196. }
  197.  
  198. if (originalUrl.indexOf('weidian.com') !== -1 || originalUrl.indexOf('koudai.com') !== -1) {
  199. return WEBSITE_WEIDIAN;
  200. }
  201.  
  202. if (originalUrl.indexOf('yupoo.com') !== -1) {
  203. return WEBSITE_YUPOO;
  204. }
  205.  
  206. return WEBSITE_UNKNOWN;
  207. };
  208.  
  209. const removeWhitespaces = (item) => item.trim().replace(/\s(?=\s)/g, '');
  210.  
  211. /**
  212. * @param input {string}
  213. * @param maxLength {number} must be an integer
  214. * @returns {string}
  215. */
  216. const truncate = function (input, maxLength) {
  217. function isHighSurrogate(codePoint) {
  218. return codePoint >= 0xd800 && codePoint <= 0xdbff;
  219. }
  220.  
  221. function isLowSurrogate(codePoint) {
  222. return codePoint >= 0xdc00 && codePoint <= 0xdfff;
  223. }
  224.  
  225. function getLength(segment) {
  226. if (typeof segment !== 'string') {
  227. throw new Error('Input must be string');
  228. }
  229.  
  230. const charLength = segment.length;
  231. let byteLength = 0;
  232. let codePoint = null;
  233. let prevCodePoint = null;
  234. for (let i = 0; i < charLength; i++) {
  235. codePoint = segment.charCodeAt(i);
  236. // handle 4-byte non-BMP chars
  237. // low surrogate
  238. if (isLowSurrogate(codePoint)) {
  239. // when parsing previous hi-surrogate, 3 is added to byteLength
  240. if (prevCodePoint != null && isHighSurrogate(prevCodePoint)) {
  241. byteLength += 1;
  242. } else {
  243. byteLength += 3;
  244. }
  245. } else if (codePoint <= 0x7f) {
  246. byteLength += 1;
  247. } else if (codePoint >= 0x80 && codePoint <= 0x7ff) {
  248. byteLength += 2;
  249. } else if (codePoint >= 0x800 && codePoint <= 0xffff) {
  250. byteLength += 3;
  251. }
  252. prevCodePoint = codePoint;
  253. }
  254.  
  255. return byteLength;
  256. }
  257.  
  258. if (typeof input !== 'string') {
  259. throw new Error('Input must be string');
  260. }
  261.  
  262. const charLength = input.length;
  263. let curByteLength = 0;
  264. let codePoint;
  265. let segment;
  266.  
  267. for (let i = 0; i < charLength; i += 1) {
  268. codePoint = input.charCodeAt(i);
  269. segment = input[i];
  270.  
  271. if (isHighSurrogate(codePoint) && isLowSurrogate(input.charCodeAt(i + 1))) {
  272. i += 1;
  273. segment += input[i];
  274. }
  275.  
  276. curByteLength += getLength(segment);
  277.  
  278. if (curByteLength === maxLength) {
  279. return input.slice(0, i + 1);
  280. }
  281. if (curByteLength > maxLength) {
  282. return input.slice(0, i - segment.length + 1);
  283. }
  284. }
  285.  
  286. return input;
  287. };
  288.  
  289. /**
  290. * @param url {string}
  291. * @returns {Promise<string>}
  292. */
  293. const toDataURL = (url) => fetch(url)
  294. .then((response) => response.blob())
  295. .then((blob) => new Promise((resolve, reject) => {
  296. const reader = new FileReader();
  297. reader.onloadend = () => resolve(reader.result);
  298. reader.onerror = reject;
  299. reader.readAsDataURL(blob);
  300. }));
  301.  
  302. /**
  303. * @param base64Data {string}
  304. * @returns {Promise<string>}
  305. */
  306. const WebpToJpg = function (base64Data) {
  307. return new Promise((resolve) => {
  308. const image = new Image();
  309. image.src = base64Data;
  310.  
  311. image.onload = () => {
  312. const canvas = document.createElement('canvas');
  313. const context = canvas.getContext('2d');
  314.  
  315. canvas.width = image.width;
  316. canvas.height = image.height;
  317. context.drawImage(image, 0, 0);
  318.  
  319. resolve(canvas.toDataURL('image/jpeg'));
  320. };
  321. });
  322. };
  323.  
  324. /**
  325. * Waits for an element satisfying selector to exist, then resolves promise with the element.
  326. * Useful for resolving race conditions.
  327. */
  328. /**
  329. * @param selector {string}
  330. * @returns {Promise<Element>}
  331. */
  332. const elementReady = function (selector) {
  333. return new Promise((resolve) => {
  334. // Check if the element already exists
  335. const element = document.querySelector(selector);
  336. if (element) {
  337. resolve(element);
  338. }
  339.  
  340. // It doesn't so, so let's make a mutation observer and wait
  341. new MutationObserver((mutationRecords, observer) => {
  342. // Query for elements matching the specified selector
  343. Array.from(document.querySelectorAll(selector)).forEach((foundElement) => {
  344. // Resolve the element that we found
  345. resolve(foundElement);
  346.  
  347. // Once we have resolved we don't need the observer anymore.
  348. observer.disconnect();
  349. });
  350. }).observe(document.documentElement, { childList: true, subtree: true });
  351. });
  352. };
  353.  
  354. class BaseTaoElement {
  355. constructor($element, data) {
  356. this.element = $element;
  357. this.data = data;
  358.  
  359. // Set the order id
  360. this.orderId = data.oid;
  361.  
  362. // Item name
  363. this.title = truncate(removeWhitespaces(data.goodsname), 255);
  364.  
  365. // Item and shipping prices
  366. this.itemPrice = `CNY ${data.goodsprice}`;
  367. this.freightPrice = `CNY ${data.sendprice}`;
  368.  
  369. // URL related stuff
  370. this.url = data.goodsurl;
  371. this.website = determineWebsite(this.url);
  372.  
  373. // QC images location
  374. this.qcImagesUrl = `https://www.basetao.com/best-taobao-agent-service/purchase/order_img/${data.oid}.html`;
  375.  
  376. // Item sizing (if any)
  377. let sizing = removeWhitespaces(data.goodssize);
  378. sizing = (sizing !== '-' && sizing.toLowerCase() !== 'no') ? truncate(sizing, 255) : '';
  379. this.sizing = sizing.length !== 0 ? sizing : null;
  380.  
  381. // Item color (if any)
  382. let color = removeWhitespaces(data.goodscolor);
  383. color = (color !== '-' && color.toLowerCase() !== 'no') ? truncate(color, 255) : '';
  384. this.color = color.length !== 0 ? color : null;
  385.  
  386. // Item weight
  387. const weight = removeWhitespaces(data.orderweight);
  388. this.weight = weight.length !== 0 ? `${weight} gram` : null;
  389.  
  390. // Image url storage, for later
  391. this.imageUrls = [];
  392.  
  393. // Set at a later date, if ever
  394. this.albumId = null;
  395. }
  396.  
  397. /**
  398. * @return {string}
  399. */
  400. get albumUrl() {
  401. return `https://imgur.com/a/${this.albumId}`;
  402. }
  403.  
  404. /**
  405. * @param imageUrls {string[]}
  406. */
  407. set images(imageUrls) {
  408. this.imageUrls = imageUrls;
  409. }
  410.  
  411. /**
  412. * @returns {string}
  413. */
  414. get purchaseUrl() {
  415. return cleanPurchaseUrl(this.url, this.website);
  416. }
  417. }
  418.  
  419. const ImgurIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACl0lEQVQ4jX2TS0jUURSHv3v/d9TRMh0tzYrQiF7qpgijaFERRhlkxSwyiggXKdEiCjLBCi10UVirau0mCgzMVQQ96EHv1GxRWWBoKTbK2H+c+2gx6ZRIZ3U238c59/yuAFjQebQSqAeKcSj+VwINdAHn+re13hJ/4Ju4/2IziQB2K6B+JlgKQSglg0wVJF2lEhAeFseAH2HQj4ADBPUKKJ6C4nBxdRVrQ0VkBjL4aWOMuTi/nEHjkEKyVGWy42EzX8eHAYrV5M7Ot7jLETa1r+TE4F0ej39DegrpKYSnkCrRN4fWsSiYkxA4lJyEo40DzPmeQnjPWc5kb2RlaghrNNZonNFYneits0ghkqs63xK7MITpjVFVtZneD1958eA9+7JLEuA0SQoS30wkBbp5hKZD+ykpLaSxqY0zDQdIX59LXV9nEvxLMksEGNX+lEB5Hw3OOYJpqVxurWVBxWJqP9xAS4n0EpGQgP0DZMkURuLR5ATRqM/J49ep2FlGwfaFHOluI6ZjOBPHGo2whtPzk28SkmkMx8b+OrcTOOeYm5tF11g/Ezo2NbK0hguLy5n7RnMpdyvr0gsYjUcxLhkcCYJdlRtoPNdGub+McP4anNEIa2lZspPYvSGuXuvgYPg8zTmbeT7y+d9AZgUr40IKZZwhLz+b9o4Gbqtuls+ez8T9CLU1V/D9CYy1hMpyEMeyMJ6bjLOWONFVUlqIcIL8vGzKt9RR9DKDwfZvVB++REtLNaWlRQRWBDE1s5Nworq8oFr1Y3BgJJyaFmBveCOPH/Vwp+MZkUiUL33fef3qE32BYdJP5SHS5PTPVOP5uud9UK16Z7Rd9vRJb64xTlrjcM4xOjrOr4WWYN28JCzQCN4CNf3bWm/9BkZ7QCBmf01HAAAAAElFTkSuQmCC';
  420. const Loading = 'data:image/gif;base64,R0lGODlhEAAQAPMAAP////r6+paWlr6+vnx8fIyMjOjo6NDQ0ISEhLa2tq6urvDw8MjIyODg4J6enqampiH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAEAAQAAAETBDISau9NQjCiUxDYGmdhBCFkRUlcLCFOA3oNgXsQG2HRh0EAYWDIU6MGSSAR1G4ghRa7KjIUXCog6QzpRhYiC1HILsOEuJxGcNuTyIAIfkECQoAAAAsAAAAABAAEAAABGIQSGkQmzjLQkTTWDAgRGmAgMGVhAIESxZwBUMgSyAUATYQPIBg8OIQJwLCQbJkdjAlUCA6KfU0VEmyGWgenpNfcCAoEo6SmWtBYtCukxhAwQKeQAYWYgAHNZIFKBoMCHcTEQAh+QQJCgAAACwAAAAAEAAQAAAEWhDIOZejGDNysgyDQBAIGWRGMa7jgAVq0TUj0lEDUZxArvAU0a1nAAQOrsnIA1gqCZ6AUzI4nAxJwIEgyAQUhCQsjDmUCI1jDEhlrQrFV+ksGLApWwYz41jsIwAh+QQJCgAAACwAAAAAEAAQAAAEThDISau9IIQahiCEMGhCQxkFqBLFZ0pBWhzSkYIvMLAb/OGTBII2+QExSEBjuexhVgrKAZGgqKKTGGFgBc00Np71cVsVDJVo5ydyJt/wCAAh+QQJCgAAACwAAAAAEAAQAAAEWhDISau9OAxBiBjBtRRdSRTGpRRHeJBFOKWALAXkAKQNoSwWBgFRQAA4Q5DkgOwwhCXBYTJAdAQAopVhWSgIjR1gcLLVQrQbrBV4CcwSA8l0Alo0yA8cw+9TIgAh+QQJCgAAACwAAAAAEAAQAAAEWhDISau9WA5CxAhWMDDAwXGFQR0IgQRgWRBF7JyEQgXzIC2MFkc1MQkonMbAhyQ0Y5pBg0MREA4UwwnBWGhoUIAC55DwaAcQrIXATgyzE/bwCQ2sBGZmz7dEAAA7';
  421.  
  422. class Imgur {
  423. /**
  424. * @param version {string}
  425. * @param config {GM_config}
  426. * @param agent {string}
  427. * @constructor
  428. */
  429. constructor(version, config, agent) {
  430. this.version = version;
  431. this.agent = agent;
  432.  
  433. if (config.get('imgurApi') === 'imgur') {
  434. this.headers = {
  435. authorization: `Client-ID ${config.get('imgurClientId')}`,
  436. 'Content-Type': 'application/json',
  437. };
  438. this.host = config.get('imgurApiHost');
  439.  
  440. return;
  441. }
  442.  
  443. if (config.get('imgurApi') === 'rapidApi') {
  444. this.headers = {
  445. authorization: `Bearer ${config.get('rapidApiBearer')}`,
  446. 'x-rapidapi-key': config.get('rapidApiKey'),
  447. 'x-rapidapi-host': config.get('rapidApiHost'),
  448. };
  449. this.host = config.get('rapidApiHost');
  450.  
  451. return;
  452. }
  453.  
  454. throw new Error('Invalid Imgur API has been chosen');
  455. }
  456.  
  457. /**
  458. * @param options
  459. * @returns {Promise<*|null>}
  460. */
  461. async CreateAlbum(options) {
  462. const requestData = {
  463. url: `https://${this.host}/3/album`,
  464. type: 'POST',
  465. headers: this.headers,
  466. data: JSON.stringify({
  467. title: options.title,
  468. }),
  469. };
  470. Sentry.addBreadcrumb({
  471. category: 'Imgur',
  472. message: 'Creating album',
  473. data: requestData,
  474. });
  475.  
  476. Logger.debug('Creating album', requestData);
  477.  
  478. return $.ajax(requestData).retry({ times: 3 }).catch((err) => {
  479. // Check if Imgur is being a bitch
  480. if (typeof err.responseJSON === 'undefined') {
  481. // Store request so we know what was asked
  482. this._storeRequestError(err);
  483.  
  484. throw new ImgurError('Could not make an album, because Imgur is returning empty responses. Please try again later...', err);
  485. }
  486.  
  487. this._handleImgurError(err);
  488. });
  489. }
  490.  
  491. /**
  492. * @param base64Image {string}
  493. * @param albumDeleteHash {string}
  494. * @param purchaseUrl {string}
  495. * @returns {Promise<boolean>}
  496. */
  497. async AddBase64ImageToAlbum(base64Image, albumDeleteHash, purchaseUrl) {
  498. // First step, upload the image
  499. const requestData = {
  500. url: `https://${this.host}/3/image`,
  501. headers: this.headers,
  502. type: 'POST',
  503. data: JSON.stringify({
  504. album: albumDeleteHash,
  505. type: 'base64',
  506. image: base64Image,
  507. description: this._getImageDescription(purchaseUrl),
  508. }),
  509. };
  510.  
  511. Logger.debug('Adding image to album', requestData);
  512. Sentry.addBreadcrumb({
  513. category: 'Imgur',
  514. message: 'Adding image to album',
  515. data: requestData,
  516. });
  517.  
  518. await $.ajax(requestData).retry({ times: 3 }).catch((err) => {
  519. // Check if Imgur is being a bitch
  520. if (typeof err.responseJSON === 'undefined') {
  521. // Store request so we know what was asked
  522. this._storeRequestError(err);
  523. }
  524.  
  525. this._handleImgurError(err);
  526. });
  527. }
  528.  
  529. /**
  530. * @param imageUrl {string}
  531. * @param albumDeleteHash {string}
  532. * @param purchaseUrl {string}
  533. * @returns {Promise<*|null>}
  534. */
  535. async AddImageToAlbum(imageUrl, albumDeleteHash, purchaseUrl) {
  536. // First step, upload the image
  537. const requestData = {
  538. url: `https://${this.host}/3/image`,
  539. headers: this.headers,
  540. type: 'POST',
  541. data: JSON.stringify({
  542. album: albumDeleteHash,
  543. image: imageUrl,
  544. description: this._getImageDescription(purchaseUrl),
  545. }),
  546. };
  547.  
  548. Logger.debug('Adding image to album', requestData);
  549. Sentry.addBreadcrumb({
  550. category: 'Imgur',
  551. message: 'Adding image to album',
  552. data: requestData,
  553. });
  554.  
  555. await $.ajax(requestData).retry({ times: 3 }).catch((err) => {
  556. // Check if Imgur is being a bitch
  557. if (typeof err.responseJSON === 'undefined') {
  558. // Store request so we know what was asked
  559. this._storeRequestError(err);
  560. }
  561.  
  562. this._handleImgurError(err);
  563. });
  564. }
  565.  
  566. /**
  567. * @param deleteHash {string}
  568. */
  569. RemoveAlbum(deleteHash) {
  570. const requestData = {
  571. url: `https://${this.host}/3/album/${deleteHash}`,
  572. headers: this.headers,
  573. type: 'DELETE',
  574. };
  575. Sentry.addBreadcrumb({
  576. category: 'Imgur',
  577. message: 'Removing album',
  578. data: requestData,
  579. });
  580.  
  581. $.ajax(requestData).retry({ times: 3 }).catch(() => {});
  582. }
  583.  
  584. _getAlbumDescription() {
  585. return `Auto uploaded using FR:Reborn - ${this.agent} ${this.version}`;
  586. }
  587.  
  588. /**
  589. * @param purchaseUrl {string}
  590. */
  591. _getImageDescription(purchaseUrl) {
  592. return purchaseUrl.length === 0 ? this._getAlbumDescription() : `W2C: ${purchaseUrl}`;
  593. }
  594.  
  595. /**
  596. * @private
  597. * @param err {Error}
  598. */
  599. _storeRequestError(err) {
  600. Sentry.addBreadcrumb({
  601. category: 'Imgur',
  602. message: `Imgur returned: '${err.statusText}'`,
  603. data: err,
  604. level: Sentry.Severity.Error,
  605. });
  606. }
  607.  
  608. /**
  609. * @private
  610. * @param err {Error}
  611. */
  612. _handleImgurError(err) {
  613. // If there is a server error, let the user now
  614. if (err.status === 503 || (err.responseJSON && err.responseJSON.status === 503)) {
  615. throw new ImgurError('Imgur is either down, over-capacity or you did too many requests. Try again later', err);
  616. }
  617.  
  618. // If we uploaded too many files, re-throw as proper error (checking via old response setup, new response setup and a simple fallback)
  619. if (err.responseJSON && err.responseJSON.data && err.responseJSON.data.error && err.responseJSON.data.error.code === 429) {
  620. throw new ImgurSlowdownError(err.responseJSON.data.error.message, err);
  621. } else if (err.responseJSON && err.responseJSON.errors && err.responseJSON.errors.length === 1 && err.responseJSON.errors[0] && err.responseJSON.errors[0].code === 429) {
  622. throw new ImgurSlowdownError(err.responseJSON.errors[0].detail, err);
  623. } else if (err.status === 429 || (err.responseJSON && err.responseJSON.status === 429)) {
  624. throw new ImgurSlowdownError('Too Many Requests', err);
  625. }
  626.  
  627. // Store request so we know what was asked
  628. this._storeRequestError(err);
  629.  
  630. // If we have error data from Imgur, throw it (checking via the old response setup and the new one)
  631. if (err.responseJSON && err.responseJSON.data && err.responseJSON.data.error) {
  632. throw new ImgurError(`An error happened when uploading the image:\n${err.responseJSON.data.error.message}`, err);
  633. } else if (err.responseJSON && err.responseJSON.errors && err.responseJSON.errors.length !== 0 && err.responseJSON.errors[0].detail) {
  634. throw new ImgurSlowdownError(`An error happened when uploading the image:\n${err.responseJSON.errors[0].detail}`, err);
  635. }
  636.  
  637. // If not, just show the full JSON
  638. throw new ImgurError(`An error happened when uploading the image:\n${err.responseJSON}`, err);
  639. }
  640. }
  641.  
  642. const buildSwaggerHTTPError = function (response) {
  643. // Build basic error (and use response as extra)
  644. const error = new Error(`${response.body.detail}: ${response.url}`);
  645.  
  646. // Add status and status code
  647. error.status = response.body.status;
  648. error.statusCode = response.body.status;
  649.  
  650. return error;
  651. };
  652.  
  653. class QC {
  654. /**
  655. * @param version {string}
  656. * @param client {SwaggerClient}
  657. * @param userHash {string}
  658. * @param identifier {string}
  659. * @param agent {string}
  660. */
  661. constructor(version, client, userHash, identifier, agent) {
  662. this.version = version;
  663. this.client = client;
  664. this.userHash = userHash;
  665. this.identifier = identifier;
  666. this.agent = agent;
  667. }
  668.  
  669. /**
  670. * @param element {BaseTaoElement|CSSBuyElement|WeGoBuyElement}
  671. * @returns {Promise<null|string>}
  672. */
  673. existingAlbumByOrderId(element) {
  674. const request = { url: element.url, orderId: element.orderId };
  675.  
  676. return this.client.apis.QualityControl.uploaded(request).then((response) => {
  677. if (typeof response.body === 'undefined') {
  678. return null;
  679. }
  680.  
  681. if (!response.body.success) {
  682. return null;
  683. }
  684.  
  685. // Force add the album ID to the element
  686. element.albumId = response.body.albumId; // eslint-disable-line no-param-reassign
  687.  
  688. return response.body.albumId;
  689. }).catch((reason) => {
  690. Logger.error(`Could not check if the album for order '${element.orderId}' exists on the QC server`, reason);
  691.  
  692. // For some reason we couldn't fetch information, just return, server probably down or something
  693. if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {
  694. return '-1';
  695. }
  696.  
  697. // Add breadcrumb with actual request we did
  698. Sentry.addBreadcrumb({
  699. category: 'Swagger',
  700. message: 'existingAlbumByOrderId',
  701. data: { request },
  702. level: Sentry.Severity.Debug,
  703. });
  704.  
  705. // Add breadcrumb with the error
  706. Sentry.addBreadcrumb({
  707. category: 'Swagger - Error',
  708. message: 'existingAlbumByOrderId',
  709. data: { error: reason },
  710. level: Sentry.Severity.Error,
  711. });
  712.  
  713. // Swagger HTTP error
  714. if (typeof reason.response !== 'undefined') {
  715. Sentry.captureException(buildSwaggerHTTPError(reason));
  716.  
  717. return '-1';
  718. }
  719.  
  720. Sentry.captureException(new Error(`Could not check if the album for order '${element.orderId}' exists on the QC server`));
  721.  
  722. return '-1';
  723. });
  724. }
  725.  
  726. /**
  727. * @param url {string}
  728. * @returns {Promise<boolean>}
  729. */
  730. exists(url) {
  731. const request = { url };
  732.  
  733. return this.client.apis.QualityControl.exists(request).then((response) => {
  734. if (typeof response.body === 'undefined') {
  735. return null;
  736. }
  737.  
  738. if (!response.body.success) {
  739. return null;
  740. }
  741.  
  742. return response.body.exists;
  743. }).catch((reason) => {
  744. Logger.error(`Could not check if any album exists on the QC server, URL: '${url}'`, reason);
  745.  
  746. // For some reason we couldn't fetch information, just return, server probably down or something
  747. if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource') || reason.message.includes('response status is 200')) {
  748. return '-1';
  749. }
  750.  
  751. // Add breadcrumb with actual request we did
  752. Sentry.addBreadcrumb({
  753. category: 'Swagger',
  754. message: 'exists',
  755. data: { request },
  756. level: Sentry.Severity.Debug,
  757. });
  758.  
  759. // Add breadcrumb with the error
  760. Sentry.addBreadcrumb({
  761. category: 'Swagger - Error',
  762. message: 'exists',
  763. data: { error: reason },
  764. level: Sentry.Severity.Error,
  765. });
  766.  
  767. // Swagger HTTP error
  768. if (typeof reason.response !== 'undefined') {
  769. Sentry.captureException(buildSwaggerHTTPError(reason));
  770.  
  771. return false;
  772. }
  773.  
  774. Sentry.captureException(new Error(`Could not check if any album exists on the QC server, URL: '${url}'`));
  775.  
  776. return false;
  777. });
  778. }
  779.  
  780. /**
  781. * @param element {BaseTaoElement|CSSBuyElement|WeGoBuyElement}
  782. * @param album {string}
  783. */
  784. uploadQc(element, album) {
  785. const request = {
  786. method: 'post',
  787. requestContentType: 'application/json',
  788. requestBody: {
  789. usernameHash: this.userHash,
  790. identifier: this.identifier,
  791. albumId: album,
  792. color: element.color,
  793. orderId: element.orderId,
  794. purchaseUrl: element.purchaseUrl,
  795. sizing: element.sizing,
  796. itemPrice: element.itemPrice,
  797. freightPrice: element.freightPrice,
  798. weight: element.weight,
  799. source: `${this.agent} to Imgur ${this.version}`,
  800. website: element.website,
  801. },
  802. };
  803.  
  804. Logger.log('Adding new QC to FR: Reborn', request);
  805.  
  806. return this.client.apis.QualityControl.postQualityControlCollection({}, request).catch((reason) => {
  807. Logger.error('Could not upload QC to the QC server', reason);
  808.  
  809. // For some reason we couldn't fetch information, just return, server probably down or something
  810. if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {
  811. return;
  812. }
  813.  
  814. // If the order already exists, just ignore the error
  815. if (reason.message.includes('orderId: This value is already used')) {
  816. return;
  817. }
  818.  
  819. // Add breadcrumb with actual request we did
  820. Sentry.addBreadcrumb({
  821. category: 'Swagger',
  822. message: 'postQualityControlCollection',
  823. data: { request, element },
  824. level: Sentry.Severity.Debug,
  825. });
  826.  
  827. // Add breadcrumb with the error
  828. Sentry.addBreadcrumb({
  829. category: 'Swagger - Error',
  830. message: 'postQualityControlCollection',
  831. data: { error: reason },
  832. level: Sentry.Severity.Error,
  833. });
  834.  
  835. // Swagger HTTP error
  836. if (typeof reason.response !== 'undefined') {
  837. Sentry.captureException(buildSwaggerHTTPError(reason.response));
  838.  
  839. return;
  840. }
  841.  
  842. Sentry.captureException(new Error('Could not upload QC to the QC server'));
  843. });
  844. }
  845. }
  846.  
  847. class BaseTao {
  848. constructor() {
  849. this.setup = false;
  850. }
  851.  
  852. /**
  853. * @param hostname {string}
  854. * @returns {boolean}
  855. */
  856. supports(hostname) {
  857. return hostname.includes('basetao.com');
  858. }
  859.  
  860. /**
  861. * @returns {string}
  862. */
  863. name() {
  864. return 'BaseTao';
  865. }
  866.  
  867. /**
  868. * @param client {Promise<SwaggerClient>}
  869. * @returns {Promise<BaseTao>}
  870. */
  871. async build(client) {
  872. // If already build before, just return
  873. if (this.setup) {
  874. return this;
  875. }
  876.  
  877. // Get the username
  878. let username = $('[aria-labelledby="profileDropdown"] a:first').text();
  879. if (typeof username === 'undefined' || username == null || username === '') {
  880. Snackbar('You need to be logged in to use this extension.');
  881. throw new Error('You need to be logged in to use this extension.');
  882. }
  883.  
  884. // Trim the username
  885. username = username.trim();
  886.  
  887. // Hash the username (and add an extra space, since this was a bug in the past)
  888. const userHash = SparkMD5.hash(`${username} `);
  889.  
  890. // Ensure we know who triggered errors
  891. Sentry.setUser({ id: userHash, username });
  892.  
  893. // Build all the clients
  894. this.imgurClient = new Imgur(GM_info.script.version, GM_config, this.name());
  895. this.qcClient = new QC(GM_info.script.version, await client, userHash, username, this.name());
  896.  
  897. // Mark that this agent has been set up
  898. this.setup = true;
  899.  
  900. return this;
  901. }
  902.  
  903. /**
  904. * @return {Promise<void>}
  905. */
  906. async process() {
  907. if (this.setup === false) {
  908. throw new Error('Agent is not setup, so cannot be used');
  909. }
  910.  
  911. // Make copy of the current this, so we can use it later
  912. const agent = this;
  913.  
  914. // Get the container (unsafe, because we want the actual jQuery table)
  915. const $container = window.unsafeWindow.jQuery('#table').first();
  916.  
  917. // Start processing once the table has been loaded
  918. elementReady('.details-tr i.bi.bi-image-fill').then(() => {
  919. const rowData = $container.bootstrapTable('getData');
  920. $container.find('i.bi.bi-image-fill').each(function () {
  921. const $element = $(this);
  922. const orderId = $element.parents('td').find('[data-row]').data('row');
  923. agent._buildElement($element, rowData.find(agent._getRowData(orderId)));
  924. });
  925.  
  926. $container.on('load-success.bs.table', (event, data) => {
  927. $container.find('i.bi.bi-image-fill').each(function () {
  928. const $element = $(this);
  929. const orderId = $element.parents('td').find('[data-row]').data('row');
  930. agent._buildElement($(this), data.rows.find(agent._getRowData(orderId)));
  931. });
  932. });
  933.  
  934. // Ensure tooltips
  935. $container.tooltip({ selector: '.qc-tooltip' });
  936. });
  937. }
  938.  
  939. /**
  940. * @private
  941. * @param $this
  942. * @param {RowData} data
  943. * @return {Promise<BaseTaoElement>}
  944. */
  945. async _buildElement($this, data) {
  946. const element = new BaseTaoElement($this, data);
  947. const $imageIcon = element.element.parents('a').first();
  948.  
  949. // Append download button if enabled
  950. if (GM_config.get('showImagesDownloadButton')) {
  951. const $download = $('<span style="cursor: pointer;padding-left: 5px;" class="bi bi-download text-orange" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Download all photos"></span>');
  952. $download.on('click', () => this._downloadHandler($download, element));
  953. $imageIcon.parent().append($download);
  954. }
  955.  
  956. // This plugin only works for certain websites, so check if element is supported
  957. if (element.website === WEBSITE_UNKNOWN) {
  958. const $upload = $(`<div><span style="cursor: pointer;"><img src="${ImgurIcon}" alt="Create a basic album"></span></div>`);
  959. $upload.find('span').first().after($('<span class="qc-marker qc-tooltip" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Not a supported URL, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">✖</span>'));
  960. $upload.on('click', () => {
  961. this._uploadHandler(element);
  962. });
  963.  
  964. $this.parents('td').first().append($upload);
  965.  
  966. return element;
  967. }
  968.  
  969. const $loading = $(`<div><span class="qc-tooltip" style="cursor: wait;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Loading..."><img src="${Loading}" alt="Loading..."></span></div>`);
  970. $this.parents('td').first().append($loading);
  971.  
  972. // Define upload object
  973. const $upload = $(`<div><span class="qc-marker qc-tooltip" style="cursor: pointer;" style="cursor: wait;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Upload your QC"><img src="${ImgurIcon}" alt="Upload your QC"></span></div>`);
  974.  
  975. // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.
  976. const albumId = await this.qcClient.existingAlbumByOrderId(element);
  977. if (albumId === '-1') {
  978. $upload.find('span').first().html($('<span class="qc-marker qc-tooltip" style="cursor:help;color:red;font-weight: bold;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="FR:Reborn returned an error or could not load your album.">⚠️</span>'));
  979.  
  980. $this.parents('td').first().append($upload);
  981. $loading.remove();
  982.  
  983. return element;
  984. }
  985.  
  986. // Have you ever uploaded a QC? If so, link to that album
  987. const $image = $upload.find('img');
  988. if (albumId !== null && albumId !== '-1') {
  989. $upload.find('span').first().after($('<span class="qc-marker qc-tooltip" style="cursor:help;margin-left:5px;color:green;font-weight: bold;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="You have uploaded a QC">✓</span>'));
  990. $image.wrap(`<a href='${element.albumUrl}' target='_blank' data-bs-toggle="tooltip" data-bs-placement="bottom" title='Go to album'></a>`);
  991. $image.removeAttr('title');
  992.  
  993. $this.parents('td').first().append($upload);
  994. $loading.remove();
  995.  
  996. return element;
  997. }
  998.  
  999. // Has anyone ever uploaded a QC, if not, show a red marker
  1000. const exists = await this.qcClient.exists(element.purchaseUrl);
  1001. if (!exists) {
  1002. $upload.find('span').first().after($('<span class="qc-marker qc-tooltip" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="No QC in database, please upload.">(!)</span>'));
  1003. $upload.on('click', () => {
  1004. this._uploadHandler(element);
  1005. });
  1006.  
  1007. $this.parents('td').first().append($upload);
  1008. $loading.remove();
  1009.  
  1010. return element;
  1011. }
  1012.  
  1013. // A previous QC exists, but you haven't uploaded yours yet, show orange marker
  1014. $upload.find('span').first().after($('<span class="qc-marker qc-tooltip" style="cursor:help;margin-left:5px;color:orange;font-weight: bold;" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Your QC is not yet in the database, please upload.">(!)</span>'));
  1015. $upload.on('click', () => {
  1016. this._uploadHandler(element);
  1017. });
  1018.  
  1019. $this.parents('td').first().append($upload);
  1020. $loading.remove();
  1021.  
  1022. return element;
  1023. }
  1024.  
  1025. /**
  1026. * @private
  1027. * @param element {BaseTaoElement}
  1028. * @returns {Promise<void>}
  1029. */
  1030. async _uploadToImgur(element) {
  1031. if (this.setup === false) {
  1032. throw new Error('Agent is not setup, so cannot be used');
  1033. }
  1034.  
  1035. if (element.imageUrls.length === 0) {
  1036. Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);
  1037. return;
  1038. }
  1039.  
  1040. const $processing = $(`<span style="cursor: wait;"><img src="${Loading}" alt="Processing..."></span>`);
  1041. const $base = element.element.parents('td').first().find('div').first();
  1042. $base.after($processing).hide();
  1043.  
  1044. // Start the process
  1045. Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);
  1046.  
  1047. // Temp store deleteHash
  1048. let deleteHash;
  1049.  
  1050. try {
  1051. // Create the album
  1052. const response = await this.imgurClient.CreateAlbum(element);
  1053. if (typeof response === 'undefined' || response == null) {
  1054. return;
  1055. }
  1056.  
  1057. // Extract and build information needed
  1058. deleteHash = response.data.deletehash;
  1059. const albumId = response.data.id;
  1060.  
  1061. // Upload all QC images
  1062. const promises = [];
  1063. $.each(element.imageUrls, (key, imageUrl) => {
  1064. // Convert to base64, since Imgur cannot access our images
  1065. promises.push(toDataURL(imageUrl).then(async (data) => {
  1066. // Store our base64 and if the file is WEBP, convert it to JPG
  1067. let base64Image = data;
  1068. if (base64Image.indexOf('image/webp') !== -1) {
  1069. base64Image = await WebpToJpg(base64Image);
  1070. }
  1071.  
  1072. // Remove the unnecessary `data:` part
  1073. const cleanedData = base64Image.replace(/(data:image\/.*;base64,)/, '');
  1074.  
  1075. // Upload the image to the album
  1076. return this.imgurClient.AddBase64ImageToAlbum(cleanedData, deleteHash, element.purchaseUrl);
  1077. }));
  1078. });
  1079.  
  1080. // Wait until everything has been tried to be uploaded
  1081. await Promise.all(promises);
  1082.  
  1083. // Set albumId in element, so we don't upload it again (when doing a pending haul upload)
  1084. element.albumId = albumId; // eslint-disable-line no-param-reassign
  1085.  
  1086. // Tell the user it was uploaded and open the album in the background
  1087. Snackbar('Pictures have been uploaded!', 'success');
  1088. GM_openInTab(element.albumUrl, true);
  1089.  
  1090. // Tell QC Suite about our uploaded QC's (if it's supported)
  1091. if (element.website !== WEBSITE_UNKNOWN) {
  1092. this.qcClient.uploadQc(element, albumId);
  1093. }
  1094.  
  1095. // Wrap the logo in a href to the new album
  1096. const $image = $base.find('img');
  1097. $image.wrap(`<a href='${element.albumUrl}' target='_blank' data-bs-toggle="tooltip" data-bs-placement="bottom" title='Go to album'></a>`);
  1098. $image.removeAttr('title');
  1099.  
  1100. // Remove processing
  1101. $processing.remove();
  1102.  
  1103. // Update the marker
  1104. const checkMarkMessage = element.website !== WEBSITE_UNKNOWN ? 'You have uploaded a QC' : 'Album has been created';
  1105. const $qcMarker = $base.find('.qc-marker:not(:first-child)').first();
  1106. $qcMarker.attr('title', checkMarkMessage)
  1107. .css('cursor', 'help')
  1108. .css('color', 'green')
  1109. .text('✓');
  1110.  
  1111. // Remove the click handler
  1112. $base.off();
  1113.  
  1114. // Show it again
  1115. $base.show();
  1116. } catch (err) {
  1117. // Remove the created album
  1118. this.imgurClient.RemoveAlbum(deleteHash);
  1119.  
  1120. // Reset the button
  1121. $processing.remove();
  1122. $base.show();
  1123.  
  1124. // Show the error
  1125. Snackbar(err.message, 'error');
  1126.  
  1127. // If it's the slow down error, don't log it
  1128. if (err instanceof ImgurSlowdownError) {
  1129. return;
  1130. }
  1131.  
  1132. // Log the error
  1133. Sentry.captureException(err);
  1134. Logger.error(err);
  1135. }
  1136. }
  1137.  
  1138. /**
  1139. * @private
  1140. * @param $download
  1141. * @param element {BaseTaoElement}
  1142. */
  1143. async _downloadHandler($download, element) {
  1144. if (this.setup === false) {
  1145. throw new Error('Agent is not setup, so cannot be used');
  1146. }
  1147.  
  1148. if (!await ConfirmDialog()) {
  1149. return;
  1150. }
  1151.  
  1152. // Remove button so people don't do dumb shit
  1153. $download.remove();
  1154.  
  1155. // Go to the QC pictures URL, grab all image sources and upload the element
  1156. await $.get(element.qcImagesUrl).then(async (data) => {
  1157. if (data.indexOf('long time no operation ,please sign in again') !== -1) {
  1158. Snackbar('You are no longer logged in, reloading page....', 'warning');
  1159. Logger.info('No longer logged in, reloading page for user...');
  1160. window.location.reload();
  1161.  
  1162. return null;
  1163. }
  1164.  
  1165. Snackbar('Zipping images, this might take a while....', 'info');
  1166.  
  1167. // Create a zip file writer
  1168. const zipWriter = new zip.ZipWriter(new zip.Data64URIWriter('application/zip'));
  1169.  
  1170. // Download all the images and add to the zip
  1171. const promises = [];
  1172. $('<div/>').html(data).find('.card > img').each(function () {
  1173. const src = $(this).attr('src');
  1174. promises.push(new Promise((resolve) => toDataURL(src)
  1175. .then((dataURI) => zipWriter.add(src.substring(src.lastIndexOf('/') + 1), new zip.Data64URIReader(dataURI)))
  1176. .then(() => resolve())));
  1177. });
  1178.  
  1179. // Wait for all images to be added to the ZIP
  1180. await Promise.all(promises);
  1181.  
  1182. // Close the ZipWriter object and download to computer
  1183. saveAs(await zipWriter.close(), `${element.orderId}.zip`);
  1184.  
  1185. Snackbar(`Downloading ${element.orderId}.zip`, 'success');
  1186.  
  1187. return null;
  1188. }).catch((err) => {
  1189. Snackbar(`Could not get all images for order ${element.orderId}`);
  1190. Logger.error(`Could not get all images for order ${element.orderId}`, err);
  1191. });
  1192. }
  1193.  
  1194. /**
  1195. * @private
  1196. * @param element {BaseTaoElement}
  1197. * @returns {Promise<void>}
  1198. */
  1199. async _uploadHandler(element) {
  1200. if (this.setup === false) {
  1201. throw new Error('Agent is not setup, so cannot be used');
  1202. }
  1203.  
  1204. // Go to the QC pictures URL, grab all image sources and upload the element
  1205. await $.get(element.qcImagesUrl).then(async (data) => {
  1206. if (data.indexOf('long time no operation ,please sign in again') !== -1) {
  1207. Snackbar('You are no longer logged in, reloading page....', 'warning');
  1208. Logger.info('No longer logged in, reloading page for user...');
  1209. window.location.reload();
  1210.  
  1211. return null;
  1212. }
  1213.  
  1214. // Add all image urls to the element
  1215. $('<div/>').html(data).find('main div.container.container img').each(function () {
  1216. element.imageUrls.push($(this).attr('src'));
  1217. });
  1218.  
  1219. // Finally go and upload the order
  1220. return this._uploadToImgur(element);
  1221. }).catch((err) => {
  1222. Snackbar(`Could not get all images for order ${element.orderId}`);
  1223. Logger.error(`Could not get all images for order ${element.orderId}`, err);
  1224. });
  1225. }
  1226.  
  1227. /**
  1228. * @private
  1229. * @param orderId
  1230. */
  1231. _getRowData(orderId) {
  1232. return (item) => Number(item.oid) === Number(orderId);
  1233. }
  1234. }
  1235.  
  1236. class CSSBuyElement {
  1237. constructor($element) {
  1238. this.element = $element;
  1239.  
  1240. // Create empty array for images
  1241. this.imageUrls = [];
  1242.  
  1243. // Temporary items
  1244. const parentTableEntry = $element.parentsUntil('tbody');
  1245. const itemLink = parentTableEntry.find('td.tabletd3 > a');
  1246. const splitText = parentTableEntry.find('td.tabletd3 > span:nth-child(3)').html().split('<br>');
  1247.  
  1248. // Order details
  1249. this.orderId = this.element.parent().attr('data-id');
  1250.  
  1251. // Item name
  1252. this.title = truncate(removeWhitespaces(itemLink.text()), 255);
  1253.  
  1254. // Purchase details
  1255. this.website = determineWebsite(itemLink.attr('href'));
  1256. this.purchaseUrl = cleanPurchaseUrl(itemLink.attr('href'), this.website);
  1257.  
  1258. // Item price
  1259. this.itemPrice = `CNY ${removeWhitespaces(parentTableEntry.find('td:nth-child(4) > span:nth-child(1)').text())}`;
  1260.  
  1261. // Freight price
  1262. this.freightPrice = `CNY ${removeWhitespaces(parentTableEntry.find('td:nth-child(5) span').text())}`;
  1263.  
  1264. // Item weight
  1265. const weight = removeWhitespaces(parentTableEntry.find('td:nth-child(7) span').text());
  1266. this.weight = weight.length !== 0 ? `${weight} gram` : null;
  1267.  
  1268. // Item sizing and color (if any)
  1269. this.color = null;
  1270. this.sizing = null;
  1271.  
  1272. try {
  1273. if (splitText.length === 1) {
  1274. let color = splitText[0].split(' : ')[1];
  1275. color = (typeof color !== 'undefined' && color !== '-' && color.toLowerCase() !== 'no') ? truncate(color, 255) : '';
  1276. this.color = color.length !== 0 ? color : null;
  1277. } else if (splitText.length === 2) {
  1278. let sizing = (splitText[0].split(' : ')[1]);
  1279. sizing = (typeof sizing !== 'undefined' && sizing !== '-' && sizing.toLowerCase() !== 'no') ? truncate(sizing, 255) : '';
  1280. this.sizing = sizing.length !== 0 ? sizing : null;
  1281.  
  1282. let color = (splitText[1].split(' : ')[1]);
  1283. color = (typeof color !== 'undefined' && color !== '-' && color.toLowerCase() !== 'no') ? truncate(color, 255) : '';
  1284. this.color = color.length !== 0 ? color : null;
  1285. } else if (splitText.length !== 0) {
  1286. this.sizing = splitText.join('\n');
  1287. }
  1288. } catch (e) {
  1289. Logger.info('Could not figure out sizing/color', e);
  1290. }
  1291.  
  1292. // Set at a later date, if ever
  1293. this.albumId = null;
  1294. }
  1295.  
  1296. /**
  1297. * @return {string}
  1298. */
  1299. get albumUrl() {
  1300. return `https://imgur.com/a/${this.albumId}`;
  1301. }
  1302. }
  1303.  
  1304. /* eslint-disable no-return-await */
  1305. class OSS {
  1306. constructor() {
  1307. this.setup = false;
  1308. this.window = window.unsafeWindow;
  1309. }
  1310.  
  1311. async build() {
  1312. if (this.setup) {
  1313. return this;
  1314. }
  1315.  
  1316. // Try and build the OSS client
  1317. try {
  1318. // Grab the OSS client
  1319. const WindowOSS = await this._waitForValue('OSS');
  1320.  
  1321. // Build the config for the bucket
  1322. const config = {
  1323. region: await this._waitForValue('c_region'),
  1324. accessKeyId: await this._waitForValue('c_accessid'),
  1325. accessKeySecret: await this._waitForValue('c_accesskey'),
  1326. bucket: await this._waitForValue('c_bucket'),
  1327. endpoint: `https://${await this._waitForValue('c_region')}.aliyuncs.com/`,
  1328. };
  1329.  
  1330. // Log the config, for ease of use
  1331. Logger.info('OSS config build', config);
  1332.  
  1333. // Set up the bucket for easy use
  1334. this.window.client = new WindowOSS.Wrapper(config);
  1335.  
  1336. // Mark as ready
  1337. this.setup = true;
  1338. } catch (e) {
  1339. throw new Error(e);
  1340. }
  1341.  
  1342. return this;
  1343. }
  1344.  
  1345. /**
  1346. * @param {string} orderId
  1347. *
  1348. * @return Promise<object>
  1349. */
  1350. async list(orderId) {
  1351. if (this.setup === false) {
  1352. throw new Error('OSS is not setup, so cannot be used');
  1353. }
  1354.  
  1355. return await this.window.client.list({
  1356. 'max-keys': 100,
  1357. prefix: `o/${orderId}/`,
  1358. });
  1359. }
  1360.  
  1361. _waitForValue(value) {
  1362. return new Promise((resolve) => {
  1363. // Check if the element already exists
  1364. if (this.window[value]) {
  1365. resolve(this.window[value]);
  1366.  
  1367. return;
  1368. }
  1369.  
  1370. const _waitForGlobal = () => {
  1371. if (this.window[value]) {
  1372. resolve(this.window[value]);
  1373.  
  1374. return;
  1375. }
  1376.  
  1377. // Wait until we have it
  1378. setTimeout(() => { _waitForGlobal(value, resolve); }, 100);
  1379. };
  1380.  
  1381. // It doesn't so, so let's start waiting for it
  1382. _waitForGlobal(value, resolve);
  1383. });
  1384. }
  1385. }
  1386.  
  1387. class CSSBuy {
  1388. constructor() {
  1389. this.setup = false;
  1390. }
  1391.  
  1392. /**
  1393. * @param hostname {string}
  1394. * @returns {boolean}
  1395. */
  1396. supports(hostname) {
  1397. return hostname.includes('cssbuy.com');
  1398. }
  1399.  
  1400. /**
  1401. * @returns {string}
  1402. */
  1403. name() {
  1404. return 'CSSBuy';
  1405. }
  1406.  
  1407. /**
  1408. * @param client {Promise<SwaggerClient>}
  1409. * @returns {Promise<CSSBuy>}
  1410. */
  1411. async build(client) {
  1412. // If already build before, just return
  1413. if (this.setup) {
  1414. return this;
  1415. }
  1416.  
  1417. // Get the username
  1418. const username = removeWhitespaces($(await $.get('/?go=m')).find('.userxinix > div:nth-child(1) > p').text());
  1419. if (typeof username === 'undefined' || username == null || username === '') {
  1420. Snackbar('You need to be logged in to use this extension.');
  1421.  
  1422. return this;
  1423. }
  1424.  
  1425. // Ensure we know who triggered the error
  1426. const userHash = SparkMD5.hash(username);
  1427. Sentry.setUser({ id: userHash, username });
  1428.  
  1429. // Build all the clients
  1430. this.imgurClient = new Imgur(GM_info.script.version, GM_config, this.name());
  1431. this.qcClient = new QC(GM_info.script.version, await client, userHash, username, this.name());
  1432.  
  1433. // Mark that this agent has been set up
  1434. this.setup = true;
  1435.  
  1436. return this;
  1437. }
  1438.  
  1439. async process() {
  1440. if (this.setup === false) {
  1441. throw new Error('Agent is not setup, so cannot be used');
  1442. }
  1443.  
  1444. // Make copy of the current this, so we can use it later
  1445. const agent = this;
  1446.  
  1447. // If there is nothing to process, just return here (so we don't try to build the OSS client and die)
  1448. const $elements = $(".oss-photo-view-button > a:contains('QC PIC')");
  1449. if ($elements.length === 0) {
  1450. return;
  1451. }
  1452.  
  1453. // Build OSS client
  1454. this.ossClient = new OSS();
  1455. await this.ossClient.build();
  1456.  
  1457. if (this.ossClient.setup === false) {
  1458. Snackbar('Could not build the OSS client, check the console for errors.');
  1459. }
  1460.  
  1461. // Add icons to all elements
  1462. $elements.each(function () { agent._buildElement($(this)); });
  1463. }
  1464.  
  1465. /**
  1466. * @private
  1467. * @param $this
  1468. * @return {Promise<void>}
  1469. */
  1470. async _buildElement($this) {
  1471. const element = new CSSBuyElement($this);
  1472.  
  1473. // Check if it has any images to begin with
  1474. const result = await this.ossClient.list(element.orderId);
  1475. if (typeof result.objects === 'undefined') {
  1476. return;
  1477. }
  1478.  
  1479. // This plugin only works for certain websites, so check if element is supported
  1480. if (element.website === WEBSITE_UNKNOWN) {
  1481. const $upload = $(`<ul class="badge-lists"><li style="cursor: pointer"><img src="${ImgurIcon}" alt="Create a basic album" style="width: 100%"></li></ul>`);
  1482. $upload.find('li').first().after($('<li class="btn btn-xs qc-marker" style="cursor:help;color:red;font-weight: bold;" title="Not a supported URL, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">✖</li>'));
  1483. $upload.on('click', () => { this._uploadToImgur(element); });
  1484.  
  1485. $this.parents('ul').first().after($upload);
  1486.  
  1487. return;
  1488. }
  1489.  
  1490. // Define column in which to show buttons
  1491. const $other = $this.parents('ul').first();
  1492.  
  1493. // Show simple loading animation
  1494. const $loading = $(`<ul class="badge-lists"><li style="cursor: wait"><img src="${Loading}" alt="Loading..." style="width: 100%"></li></ul>`);
  1495. $other.after($loading);
  1496.  
  1497. // Define upload object
  1498. const $upload = $(`<ul class="badge-lists"><li class="btn btn-xs qc-marker" style="cursor: pointer"><img src="${ImgurIcon}" alt="Upload your QC" style="width: 100%"></li></ul>`);
  1499.  
  1500. // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.
  1501. const albumId = await this.qcClient.existingAlbumByOrderId(element);
  1502. if (albumId === '-1') {
  1503. $upload.find('li').first().after($('<li class="btn btn-xs qc-marker" style="cursor:help;color:red;font-weight: bold;" title="FR:Reborn returned an error, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">⚠️</li>'));
  1504. $upload.on('click', () => { this._uploadToImgur(element); });
  1505.  
  1506. $other.after($upload);
  1507. $loading.remove();
  1508.  
  1509. return;
  1510. }
  1511.  
  1512. // Have you ever uploaded a QC? If so, link to that album
  1513. const $image = $upload.find('img');
  1514. if (albumId !== null && albumId !== '-1') {
  1515. $upload.find('li').first().after($('<li class="btn btn-xs qc-marker" style="cursor:help;color:green;font-weight: bold;" title="You have uploaded a QC">✓</li>'));
  1516. $image.wrap(`<a href='https://imgur.com/a/${albumId}' target='_blank' title='Go to album'></a>`);
  1517. $image.removeAttr('title');
  1518.  
  1519. $other.after($upload);
  1520. $loading.remove();
  1521.  
  1522. return;
  1523. }
  1524.  
  1525. // Has anyone ever uploaded a QC, if not, show a red marker
  1526. const exists = await this.qcClient.exists(element.purchaseUrl);
  1527. if (!exists) {
  1528. $upload.find('li').first().after($('<li class="btn btn-xs qc-marker" style="cursor:help;color:red;font-weight: bold;" title="No QC in database, please upload.">(!)</li>'));
  1529. $upload.on('click', () => { this._uploadToImgur(element); });
  1530.  
  1531. $other.after($upload);
  1532. $loading.remove();
  1533.  
  1534. return;
  1535. }
  1536.  
  1537. // A previous QC exists, but you haven't uploaded yours yet, show orange marker
  1538. $upload.find('li').first().after($('<li class="btn btn-xs qc-marker" style="cursor:help;color:orange;font-weight: bold;" title="Your QC is not yet in the database, please upload.">(!)</li>'));
  1539. $upload.on('click', () => { this._uploadToImgur(element); });
  1540.  
  1541. $other.after($upload);
  1542. $loading.remove();
  1543. }
  1544.  
  1545. /**
  1546. * @param element {CSSBuyElement}
  1547. * @returns {Promise<void>}
  1548. */
  1549. async _uploadToImgur(element) {
  1550. if (this.setup === false) {
  1551. throw new Error('Agent is not setup, so cannot be used');
  1552. }
  1553.  
  1554. const $processing = $(`<ul class="badge-lists"><li style="cursor: wait"><img src="${Loading}" alt="Processing..." style="width: 100%"></li></ul>`);
  1555. const $base = element.element.parents('td').first().find('.badge-lists');
  1556. $base.after($processing).hide();
  1557.  
  1558. const result = await this.ossClient.list(element.orderId);
  1559. if (typeof result.objects === 'undefined') {
  1560. Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);
  1561. return;
  1562. }
  1563.  
  1564. result.objects.forEach((item) => {
  1565. element.imageUrls.push((item.url));
  1566. });
  1567.  
  1568. if (element.imageUrls.length === 0) {
  1569. Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);
  1570. return;
  1571. }
  1572.  
  1573. // Start the process
  1574. Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);
  1575.  
  1576. // Temp store deleteHash
  1577. let deleteHash;
  1578.  
  1579. try {
  1580. // Create the album
  1581. const response = await this.imgurClient.CreateAlbum(element);
  1582. if (typeof response === 'undefined' || response == null) {
  1583. return;
  1584. }
  1585.  
  1586. // Extract and build information needed
  1587. deleteHash = response.data.deletehash;
  1588. const albumId = response.data.id;
  1589.  
  1590. // Upload all QC images
  1591. const promises = [];
  1592. $.each(element.imageUrls, (key, imageUrl) => {
  1593. promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl));
  1594. });
  1595.  
  1596. // Wait until everything has been tried to be uploaded
  1597. await Promise.all(promises);
  1598.  
  1599. // Set albumId in element, so we don't upload it again (when doing a pending haul upload)
  1600. element.albumId = albumId; // eslint-disable-line no-param-reassign
  1601.  
  1602. // Tell the user it was uploaded and open the album in the background
  1603. Snackbar('Pictures have been uploaded!', 'success');
  1604. GM_openInTab(element.albumUrl, true);
  1605.  
  1606. // Tell QC Suite about our uploaded QC's (if it's supported)
  1607. if (element.website !== WEBSITE_UNKNOWN) {
  1608. this.qcClient.uploadQc(element, albumId);
  1609. }
  1610.  
  1611. // Wrap the logo in a href to the new album
  1612. const $image = $base.find('img');
  1613. $image.wrap(`<a href='${element.albumUrl}' target='_blank' title='Go to album'></a>`);
  1614. $image.removeAttr('title');
  1615.  
  1616. // Remove processing
  1617. $processing.remove();
  1618.  
  1619. // Update the marker
  1620. const checkMarkMessage = element.website !== WEBSITE_UNKNOWN ? 'You have uploaded a QC' : 'Album has been created';
  1621. const $qcMarker = $base.find('.qc-marker:not(:first-child)').first();
  1622. $qcMarker.attr('title', checkMarkMessage)
  1623. .css('cursor', 'help')
  1624. .css('color', 'green')
  1625. .text('✓');
  1626.  
  1627. // Remove the click handler
  1628. $base.off();
  1629.  
  1630. // Show it again
  1631. $base.show();
  1632. } catch (err) {
  1633. // Remove the created album
  1634. this.imgurClient.RemoveAlbum(deleteHash);
  1635.  
  1636. // Reset the button
  1637. $processing.remove();
  1638. $base.show();
  1639.  
  1640. // Show the error
  1641. Snackbar(err.message, 'error');
  1642.  
  1643. // If it's the slow down error, don't log it
  1644. if (err instanceof ImgurSlowdownError) {
  1645. return;
  1646. }
  1647.  
  1648. // Log the error
  1649. Sentry.captureException(err);
  1650. Logger.error(err);
  1651. }
  1652. }
  1653. }
  1654.  
  1655. class WeGoBuyElement {
  1656. constructor($element) {
  1657. this.element = $element;
  1658.  
  1659. // Order details
  1660. this.orderId = removeWhitespaces($element.find('td:nth-child(1) > p').text());
  1661. this.imageUrls = $element.find('.lookPic').map((key, value) => $(value).attr('href')).get();
  1662.  
  1663. // Item name
  1664. this.title = truncate(removeWhitespaces($element.find('.js-item-title').text()), 255);
  1665.  
  1666. // Item sizing (if any)
  1667. const sizing = removeWhitespaces($element.find('.user_orderlist_txt').text());
  1668. this.sizing = sizing.length !== 0 ? truncate(sizing, 255) : null;
  1669.  
  1670. // Item color (WeGoBuy doesn't support separation of color, so just null)
  1671. this.color = null;
  1672.  
  1673. // Item price
  1674. const itemPriceMatches = truncate(removeWhitespaces($element.parents('tbody').find('tr > td:nth-child(2)').text()), 255).match(/([A-Z]{2})\W+([.,0-9]+)/);
  1675. this.itemPrice = `${itemPriceMatches[1]} ${itemPriceMatches[2]}`;
  1676.  
  1677. // Freight price
  1678. const freightPriceMatches = truncate(removeWhitespaces($element.parents('tbody').find('tr:nth-child(1) > td:nth-child(8) > p:nth-child(2)').text()), 255).match(/([A-Z]{2})\W+([.,0-9]+)/);
  1679. this.freightPrice = `${freightPriceMatches[1]} ${freightPriceMatches[2]}`;
  1680.  
  1681. // Item weight
  1682. this.weight = null;
  1683.  
  1684. // Purchase details
  1685. const possibleUrl = removeWhitespaces($element.find('.js-item-title').attr('href')).trim();
  1686. this.url = isUrl(possibleUrl) ? possibleUrl : '';
  1687. this.website = determineWebsite(this.url);
  1688.  
  1689. // Set at a later date, if ever
  1690. this.albumId = null;
  1691. }
  1692.  
  1693. /**
  1694. * @return {string}
  1695. */
  1696. get albumUrl() {
  1697. return `https://imgur.com/a/${this.albumId}`;
  1698. }
  1699.  
  1700. /**
  1701. * @param imageUrls {string[]}
  1702. */
  1703. set images(imageUrls) {
  1704. this.imageUrls = imageUrls;
  1705. }
  1706.  
  1707. /**
  1708. * @returns {string}
  1709. */
  1710. get purchaseUrl() {
  1711. return cleanPurchaseUrl(this.url, this.website);
  1712. }
  1713. }
  1714.  
  1715. class WeGoBuy {
  1716. constructor() {
  1717. this.setup = false;
  1718. }
  1719.  
  1720. /**
  1721. * @param hostname {string}
  1722. * @returns {boolean}
  1723. */
  1724. supports(hostname) {
  1725. return hostname.includes('wegobuy.com')
  1726. || hostname.includes('superbuy.com');
  1727. }
  1728.  
  1729. /**
  1730. * @returns {string}
  1731. */
  1732. name() {
  1733. return 'WeGoBuy';
  1734. }
  1735.  
  1736. /**
  1737. * @param client {Promise<SwaggerClient>}
  1738. * @returns {Promise<WeGoBuy>}
  1739. */
  1740. async build(client) {
  1741. // If already build before, just return
  1742. if (this.setup) {
  1743. return this;
  1744. }
  1745.  
  1746. // Ensure the toast looks decent on SB/WGB
  1747. GM_addStyle('.swal2-popup.swal2-toast .swal2-title {font-size: 1em; font-weight: bolder}');
  1748.  
  1749. // Get the username
  1750. const username = (await $.get('/ajax/user-info')).data.user_name;
  1751. if (typeof username === 'undefined' || username == null || username === '') {
  1752. Snackbar('You need to be logged in to use this extension.');
  1753.  
  1754. return this;
  1755. }
  1756.  
  1757. // Ensure we know who triggered the error
  1758. const userHash = SparkMD5.hash(username);
  1759. Sentry.setUser({ id: userHash, username });
  1760.  
  1761. // Build all the clients
  1762. this.imgurClient = new Imgur(GM_info.script.version, GM_config, this.name());
  1763. this.qcClient = new QC(GM_info.script.version, await client, userHash, username, this.name());
  1764.  
  1765. // Mark that this agent has been setup
  1766. this.setup = true;
  1767.  
  1768. return this;
  1769. }
  1770.  
  1771. async process() {
  1772. if (this.setup === false) {
  1773. throw new Error('Agent is not setup, so cannot be used');
  1774. }
  1775.  
  1776. // Make copy of the current this, so we can use it later
  1777. const agent = this;
  1778.  
  1779. // Add icons to all elements
  1780. $('.pic-list.j_picList').each(function () {
  1781. agent._buildElement($(this).parents('tr'));
  1782. });
  1783. }
  1784.  
  1785. /**
  1786. * @private
  1787. * @param $this
  1788. * @return {Promise<void>}
  1789. */
  1790. async _buildElement($this) {
  1791. const element = new WeGoBuyElement($this);
  1792.  
  1793. // No pictures (like rehearsal orders), no QC options
  1794. if (element.imageUrls.length === 0) {
  1795. return;
  1796. }
  1797.  
  1798. // Define column in which to download button
  1799. const $inspection = $this.find('td:nth-child(6)').first();
  1800.  
  1801. // Append download button if enabled
  1802. if (GM_config.get('showImagesDownloadButton')) {
  1803. const $download = $('<div style="padding:5px;"><p><span style="color: rgb(255, 140, 60);cursor: pointer;margin-top: 5px;">Download</span></p></div>');
  1804. $download.on('click', () => this._downloadHandler($download, element));
  1805. $inspection.append($download);
  1806. }
  1807.  
  1808. // This plugin only works for certain websites, so check if element is supported
  1809. if (element.website === WEBSITE_UNKNOWN || element.url.length === 0) {
  1810. const $upload = $(`<div style="padding:5px;"><span style="cursor: pointer;"><img src="${ImgurIcon}" alt="Create a basic album"></span></div>`);
  1811. $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="Not a supported URL, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">✖</span>'));
  1812. $upload.on('click', () => {
  1813. this._uploadToImgur(element);
  1814. });
  1815.  
  1816. $inspection.append($upload);
  1817.  
  1818. return;
  1819. }
  1820.  
  1821. // Show simple loading animation
  1822. const $loading = $(`<div style="padding:5px;"><span style="cursor: wait;"><img src="${Loading}" alt="Loading..."></span></div>`);
  1823. $inspection.append($loading);
  1824.  
  1825. // Define upload object
  1826. const $upload = $(`<div style="padding:5px;"><span class="qc-marker" style="cursor: pointer;"><img src="${ImgurIcon}" alt="Upload your QC"></span></div>`);
  1827.  
  1828. // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.
  1829. const albumId = await this.qcClient.existingAlbumByOrderId(element);
  1830. if (albumId === '-1') {
  1831. $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="FR:Reborn returned an error, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">⚠️</span>'));
  1832. $upload.on('click', () => {
  1833. this._uploadToImgur(element);
  1834. });
  1835.  
  1836. $inspection.append($upload);
  1837. $loading.remove();
  1838.  
  1839. return;
  1840. }
  1841.  
  1842. // Have you ever uploaded a QC? If so, link to that album
  1843. const $image = $upload.find('img');
  1844. if (albumId !== null && albumId !== '-1') {
  1845. $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:green;font-weight: bold;" title="You have uploaded a QC">✓</span>'));
  1846. $image.wrap(`<a href='https://imgur.com/a/${albumId}' target='_blank' title='Go to album' style="display: initial;"></a>`);
  1847. $image.removeAttr('title');
  1848.  
  1849. $inspection.append($upload);
  1850. $loading.remove();
  1851.  
  1852. return;
  1853. }
  1854.  
  1855. // Has anyone ever uploaded a QC, if not, show a red marker
  1856. const exists = await this.qcClient.exists(element.purchaseUrl);
  1857. if (!exists) {
  1858. $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="No QC in database, please upload.">(!)</span>'));
  1859. $upload.on('click', () => {
  1860. this._uploadToImgur(element);
  1861. });
  1862.  
  1863. $inspection.append($upload);
  1864. $loading.remove();
  1865.  
  1866. return;
  1867. }
  1868.  
  1869. // A previous QC exists, but you haven't uploaded yours yet, show orange marker
  1870. $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:orange;font-weight: bold;" title="Your QC is not yet in the database, please upload.">(!)</span>'));
  1871. $upload.on('click', () => {
  1872. this._uploadToImgur(element);
  1873. });
  1874.  
  1875. $inspection.append($upload);
  1876. $loading.remove();
  1877. }
  1878.  
  1879. /**
  1880. * @private
  1881. * @param $download
  1882. * @param element {WeGoBuyElement}
  1883. */
  1884. async _downloadHandler($download, element) {
  1885. if (this.setup === false) {
  1886. throw new Error('Agent is not setup, so cannot be used');
  1887. }
  1888.  
  1889. if (!await ConfirmDialog()) {
  1890. return;
  1891. }
  1892.  
  1893. // Remove button so people don't do dumb shit
  1894. $download.remove();
  1895.  
  1896. Snackbar('Zipping images, this might take a while....', 'info');
  1897.  
  1898. // Create a zip file writer
  1899. const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'));
  1900.  
  1901. // Download all the images and add to the zip
  1902. const promises = [];
  1903. $.each(element.imageUrls, (key, imageUrl) => {
  1904. promises.push(toDataURL(imageUrl.replace('http://', 'https://'))
  1905. .then((dataURI) => zipWriter.add(imageUrl.substring(imageUrl.lastIndexOf('/') + 1), new zip.Data64URIReader(dataURI))));
  1906. });
  1907.  
  1908. // Wait for all images to be added to the ZIP
  1909. await Promise.all(promises);
  1910.  
  1911. // Close the ZipWriter object and download to computer
  1912. saveAs(await zipWriter.close(), `${element.orderId}.zip`);
  1913.  
  1914. Snackbar(`Downloading ${element.orderId}.zip`, 'success');
  1915. }
  1916.  
  1917. /**
  1918. * @param element {WeGoBuyElement}
  1919. * @returns {Promise<void>}
  1920. */
  1921. async _uploadToImgur(element) {
  1922. if (this.setup === false) {
  1923. throw new Error('Agent is not setup, so cannot be used');
  1924. }
  1925.  
  1926. const $processing = $(`<div style="padding:5px;"><span style="cursor: wait;"><img src="${Loading}" alt="Processing..."></span></div>`);
  1927. const $options = element.element.find('td:nth-child(6)').first();
  1928. const $base = $options.find('div').last();
  1929. $base.after($processing).hide();
  1930.  
  1931. // Start the process
  1932. Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);
  1933.  
  1934. // Temp store deleteHash
  1935. let deleteHash;
  1936.  
  1937. try {
  1938. // Create the album
  1939. const response = await this.imgurClient.CreateAlbum(element);
  1940. if (typeof response === 'undefined' || response == null) {
  1941. return;
  1942. }
  1943.  
  1944. // Extract and build information needed
  1945. deleteHash = response.data.deletehash;
  1946. const albumId = response.data.id;
  1947.  
  1948. // Upload all QC images
  1949. const promises = [];
  1950. $.each(element.imageUrls, (key, imageUrl) => {
  1951. promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl));
  1952. });
  1953.  
  1954. // Wait until everything has been tried to be uploaded
  1955. await Promise.all(promises);
  1956.  
  1957. // Set albumId in element, so we don't upload it again (when doing a pending haul upload)
  1958. element.albumId = albumId; // eslint-disable-line no-param-reassign
  1959.  
  1960. // Tell the user it was uploaded and open the album in the background
  1961. Snackbar('Pictures have been uploaded!', 'success');
  1962. GM_openInTab(element.albumUrl, true);
  1963.  
  1964. // Tell QC Suite about our uploaded QC's (if it's supported)
  1965. if (element.website !== WEBSITE_UNKNOWN) {
  1966. this.qcClient.uploadQc(element, albumId);
  1967. }
  1968.  
  1969. // Remove processing
  1970. $processing.remove();
  1971. $base.remove();
  1972.  
  1973. // Add new buttons
  1974. const checkMarkMessage = element.website !== WEBSITE_UNKNOWN ? 'You have uploaded a QC' : 'Album has been created';
  1975. $options.append($('<div style="padding:5px;">'
  1976. + `<span class="qc-marker" style="cursor:pointer;"><a href='${element.albumUrl}' target='_blank' title='Go to album' style="display: initial;"><img src="${ImgurIcon}" alt="Go to album"></a></span>`
  1977. + `<span class="qc-marker" style="cursor:help;margin-left:5px;color:green;font-weight: bold;" title="${checkMarkMessage}">✓</span>`
  1978. + '</div>'));
  1979.  
  1980. // Remove the click handler
  1981. $base.off();
  1982.  
  1983. // Show it again
  1984. $base.show();
  1985. } catch (err) {
  1986. // Remove the created album
  1987. this.imgurClient.RemoveAlbum(deleteHash);
  1988.  
  1989. // Reset the button
  1990. $processing.remove();
  1991. $base.show();
  1992.  
  1993. // Show the error
  1994. Snackbar(err.message, 'error');
  1995.  
  1996. // If it's the slow down error, don't log it
  1997. if (err instanceof ImgurSlowdownError) {
  1998. return;
  1999. }
  2000.  
  2001. // Log the error
  2002. Sentry.captureException(err);
  2003. Logger.error(err);
  2004. }
  2005. }
  2006. }
  2007.  
  2008. /**
  2009. * @param hostname {string}
  2010. *
  2011. * @returns {BaseTao|CSSBuy|WeGoBuy|null}
  2012. */
  2013. function getAgent(hostname) {
  2014. const agents = [new BaseTao(), new CSSBuy(), new WeGoBuy()];
  2015.  
  2016. let agent = null;
  2017. Object.values(agents).forEach((value) => {
  2018. if (agent == null && value.supports(hostname)) {
  2019. agent = value;
  2020. }
  2021. });
  2022.  
  2023. return agent;
  2024. }
  2025.  
  2026. // Inject snackbar css style
  2027. GM_addStyle(GM_getResourceText('sweetalert2'));
  2028.  
  2029. // Setup proper settings menu
  2030. GM_config.init('Settings', {
  2031. serverSection: {
  2032. label: 'QC Server settings',
  2033. type: 'section',
  2034. },
  2035. swaggerDocUrl: {
  2036. label: 'Swagger documentation URL',
  2037. type: 'text',
  2038. default: 'https://www.fashionreps.page/api/doc.json',
  2039. },
  2040. generalSection: {
  2041. label: 'General options',
  2042. type: 'section',
  2043. },
  2044. showImagesDownloadButton: {
  2045. label: 'Show the images download button/text',
  2046. type: 'checkbox',
  2047. default: 'true',
  2048. },
  2049. uploadSection: {
  2050. label: 'Upload API Options',
  2051. type: 'section',
  2052. },
  2053. imgurApi: {
  2054. label: 'Select your Imgur API',
  2055. type: 'radio',
  2056. default: 'imgur',
  2057. options: {
  2058. imgur: 'Imgur API (Free)',
  2059. rapidApi: 'RapidAPI (Freemium)',
  2060. },
  2061. },
  2062. imgurSection: {
  2063. label: 'Imgur Options',
  2064. type: 'section',
  2065. },
  2066. imgurApiHost: {
  2067. label: 'Imgur host',
  2068. type: 'text',
  2069. default: 'api.imgur.com',
  2070. },
  2071. imgurClientId: {
  2072. label: 'Imgur Client-ID',
  2073. type: 'text',
  2074. default: 'e4e18b5ab582b4c',
  2075. },
  2076. rapidApiSection: {
  2077. label: 'RadidAPI Options',
  2078. type: 'section',
  2079. },
  2080. rapidApiHost: {
  2081. label: 'RapidAPI host',
  2082. type: 'text',
  2083. default: 'imgur-apiv3.p.rapidapi.com',
  2084. },
  2085. rapidApiKey: {
  2086. label: 'RapidAPI key (only needed if RapidApi select above)',
  2087. type: 'text',
  2088. default: '',
  2089. },
  2090. rapidApiBearer: {
  2091. label: 'RapidAPI access token (only needed if RapidApi select above)',
  2092. type: 'text',
  2093. default: '',
  2094. },
  2095. });
  2096.  
  2097. // Reload page if config changed
  2098. GM_config.onclose = (saveFlag) => {
  2099. if (saveFlag) {
  2100. window.location.reload();
  2101. }
  2102. };
  2103.  
  2104. // Register menu within GM
  2105. GM_registerMenuCommand('Settings', GM_config.open);
  2106.  
  2107. // Setup Sentry for proper error logging, which will make my lives 20x easier, use XHR since fetch dies.
  2108. Sentry.init({
  2109. dsn: 'https://474c3febc82e44b8b283f23dacb76444@o740964.ingest.sentry.io/5802425',
  2110. tunnel: 'https://www.fashionreps.page/sentry/tunnel',
  2111. transport: Sentry.Transports.XHRTransport,
  2112. release: GM_info.script.version,
  2113. defaultIntegrations: false,
  2114. integrations: [
  2115. new Sentry.Integrations.InboundFilters(),
  2116. new Sentry.Integrations.FunctionToString(),
  2117. new Sentry.Integrations.LinkedErrors(),
  2118. new Sentry.Integrations.UserAgent(),
  2119. ],
  2120. environment: 'production',
  2121. normalizeDepth: 5,
  2122. });
  2123.  
  2124. // eslint-disable-next-line func-names
  2125. (async function () {
  2126. // Setup the logger.
  2127. Logger.useDefaults();
  2128.  
  2129. // Log the start of the script.
  2130. Logger.info(`Starting extension '${GM_info.script.name}', version ${GM_info.script.version}`);
  2131.  
  2132. // Get the proper agent, if any
  2133. const agent = getAgent(window.location.hostname);
  2134. if (agent === null) {
  2135. Sentry.captureMessage(`Unsupported website ${window.location.hostname}`);
  2136. Logger.error('Unsupported website');
  2137.  
  2138. return;
  2139. }
  2140.  
  2141. Logger.info(`Agent '${agent.name()}' detected`);
  2142.  
  2143. // Finally, try to build the proper agent and process the page
  2144. try {
  2145. await agent.build(new SwaggerClient({ url: GM_config.get('swaggerDocUrl') }));
  2146. await agent.process();
  2147. } catch (error) {
  2148. if (error.message.includes('Failed to fetch') || error.message.includes('attempting to fetch resource')) {
  2149. Snackbar('We are unable to connect to FR:Reborn, features will be disabled.');
  2150. Logger.error(`We are unable to connect to FR:Reborn: ${GM_config.get('swaggerDocUrl')}`, error);
  2151.  
  2152. return;
  2153. }
  2154.  
  2155. Snackbar(`An unknown issue has occurred when trying to setup the agent extension: ${error.message}`);
  2156. Logger.error('An unknown issue has occurred', error);
  2157. }
  2158. }());

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址