Greasy Fork is available in English.
Upload QCs from your favorite agent to Imgur + QC server
// ==UserScript==// @name FR:Reborn - Agents extension// @namespace https://www.reddit.com/user/RobotOilInc// @version 2.3.4// @description Upload QCs from your favorite agent to Imgur + QC server// @author RobotOilInc// @match https://www.basetao.com/*my_account/order/*// @match https://basetao.com/*my_account/order/*// @match https://www.cssbuy.com/*name=orderlist*// @match https://cssbuy.com/*name=orderlist*// @match https://superbuy.com/order*// @match https://www.superbuy.com/order*// @match https://wegobuy.com/order*// @match https://www.wegobuy.com/order*// @grant GM_addStyle// @grant GM_getResourceText// @grant GM_getValue// @grant GM_setValue// @grant GM_openInTab// @grant GM_registerMenuCommand// @license MIT// @homepageURL https://www.fashionreps.page/// @supportURL https://gfork.dahi.icu/en/scripts/426977-fr-reborn-agents-extension// @include https://www.basetao.com/index/orderphoto/itemimg/*// @include https://basetao.com/index/orderphoto/itemimg/*// @require https://unpkg.com/[email protected]/dist/sweetalert2.js// @require https://unpkg.com/[email protected]/src/logger.js// @require https://unpkg.com/[email protected]/spark-md5.js// @require https://unpkg.com/@zip.js/[email protected]/dist/zip-full.js// @require https://unpkg.com/[email protected]/dist/FileSaver.js// @require https://unpkg.com/[email protected]/dist/jquery.js// @require https://unpkg.com/[email protected]/src/jquery.ajax-retry.js// @require https://unpkg.com/@sentry/[email protected]/build/bundle.js// @require https://unpkg.com/@sentry/[email protected]/build/bundle.tracing.js// @require https://unpkg.com/[email protected]/dist/swagger-client.browser.js// @require https://gfork.dahi.icu/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657// @resource sweetalert2 https://unpkg.com/[email protected]/dist/sweetalert2.min.css// @run-at document-end// @icon https://i.imgur.com/mYBHjAg.png// ==/UserScript==// Define default toastconst Toast = Swal.mixin({showConfirmButton: false,timerProgressBar: true,position: 'top-end',timer: 4000,toast: true,didOpen: (toast) => {toast.addEventListener('mouseenter', Swal.stopTimer);toast.addEventListener('mouseleave', Swal.r###meTimer);},});/*** @param text {string}* @param type {null|('success'|'error'|'warning'|'info')}*/const Snackbar = function (text, type = null) {Toast.fire({ title: text, icon: type != null ? type : 'info' });};/*** @return {Promise<boolean>}*/const ConfirmDialog = async function () {return new Promise((resolve) => {Swal.fire({title: 'Are you sure?',icon: 'warning',showCancelButton: true,confirmButtonColor: '#3085d6',cancelButtonColor: '#d33',confirmButtonText: 'Yes',}).then((r###lt) => resolve(r###lt.isConfirmed));});};class ImgurError extends Error {/*** @param message {string}* @param previous {Error}*/constructor(message, previous) {super(message);this.name = 'ImgurError';this.previous = previous;}}class ImgurSlowdownError extends ImgurError {constructor(message, previous) {super(`Imgur is telling us to slow down:\n${message}`, previous);}}// Possible websitesconst WEBSITE_1688 = '1688';const WEBSITE_TAOBAO = 'taobao';const WEBSITE_TMALL = 'tmall';const WEBSITE_YUPOO = 'yupoo';const WEBSITE_WEIDIAN = 'weidian';const WEBSITE_XIANYU = 'xianyu';const WEBSITE_UNKNOWN = 'unknown';/*** @internal* @param url {string}* @returns {string}*/const ensureNonEncodedURL = (url) => {if (url === decodeURIComponent(url || '')) {return url;}// Grab the encoded URLconst encodedURL = new URL(url).searchParams.get('url') || '';if (encodedURL.length === 0) {return url;}// Decode said encoded URLconst decodedURL = decodeURIComponent(encodedURL);if (decodedURL.length === 0) {return url;}return decodedURL;};/*** @param url {string}* @returns {boolean}*/const isUrl = (url) => { try { return Boolean(new URL(url)); } catch (e) { return false; } };/*** @param originalUrl {string}* @param website {string}* @returns {string}*/const cleanPurchaseUrl = (originalUrl, website) => {const url = ensureNonEncodedURL(originalUrl);const idMatches = url.match(/[?&]id=(\d+)|[?&]itemID=(\d+)|\/?[albums]\/(\d+)|offer\/(\d+)/i);const authorMatches = url.match(/https?:\/\/(.+)\.x\.yupoo\.com/);if (website === WEBSITE_TAOBAO && idMatches[1].length !== 0) {return `https://item.taobao.com/item.htm?id=${idMatches[1]}`;}if (website === WEBSITE_TMALL && idMatches[1].length !== 0) {return `https://detail.tmall.com/item.htm?id=${idMatches[1]}`;}if (website === WEBSITE_XIANYU && idMatches[1].length !== 0) {return `https://2.taobao.com/item.htm?id=${idMatches[1]}`;}if (website === WEBSITE_WEIDIAN && idMatches[2].length !== 0) {return `https://weidian.com/item.html?itemID=${idMatches[2]}`;}if (website === WEBSITE_YUPOO && idMatches[3].length !== 0 && authorMatches[1].length !== 0) {return `https://${authorMatches[1]}.x.yupoo.com/albums/${idMatches[3]}`;}if (website === WEBSITE_1688 && idMatches[4].length !== 0) {return `https://detail.1688.com/offer/${idMatches[4]}.html`;}// Just return the original URL with some clean upreturn originalUrl.replace('http://', 'https://').replace('?uid=1', '').trim();};/*** @param originalUrl {string}* @returns {string}*/const determineWebsite = (originalUrl) => {if (originalUrl.indexOf('1688.com') !== -1) {return WEBSITE_1688;}// Check more specific taobao firstif (originalUrl.indexOf('market.m.taobao.com') !== -1 || originalUrl.indexOf('2.taobao.com') !== -1) {return WEBSITE_XIANYU;}if (originalUrl.indexOf('taobao.com') !== -1) {return WEBSITE_TAOBAO;}if (originalUrl.indexOf('detail.tmall.com') !== -1) {return WEBSITE_TMALL;}if (originalUrl.indexOf('weidian.com') !== -1 || originalUrl.indexOf('koudai.com') !== -1) {return WEBSITE_WEIDIAN;}if (originalUrl.indexOf('yupoo.com') !== -1) {return WEBSITE_YUPOO;}return WEBSITE_UNKNOWN;};const removeWhitespaces = (item) => item.trim().replace(/\s(?=\s)/g, '');/*** @param input {string}* @param maxLength {number} must be an integer* @returns {string}*/const truncate = function (input, maxLength) {function isHighSurrogate(codePoint) {return codePoint >= 0xd800 && codePoint <= 0xdbff;}function isLowSurrogate(codePoint) {return codePoint >= 0xdc00 && codePoint <= 0xdfff;}function getLength(segment) {if (typeof segment !== 'string') {throw new Error('Input must be string');}const charLength = segment.length;let byteLength = 0;let codePoint = null;let prevCodePoint = null;for (let i = 0; i < charLength; i++) {codePoint = segment.charCodeAt(i);// handle 4-byte non-BMP chars// low surrogateif (isLowSurrogate(codePoint)) {// when parsing previous hi-surrogate, 3 is added to byteLengthif (prevCodePoint != null && isHighSurrogate(prevCodePoint)) {byteLength += 1;} else {byteLength += 3;}} else if (codePoint <= 0x7f) {byteLength += 1;} else if (codePoint >= 0x80 && codePoint <= 0x7ff) {byteLength += 2;} else if (codePoint >= 0x800 && codePoint <= 0xffff) {byteLength += 3;}prevCodePoint = codePoint;}return byteLength;}if (typeof input !== 'string') {throw new Error('Input must be string');}const charLength = input.length;let curByteLength = 0;let codePoint;let segment;for (let i = 0; i < charLength; i += 1) {codePoint = input.charCodeAt(i);segment = input[i];if (isHighSurrogate(codePoint) && isLowSurrogate(input.charCodeAt(i + 1))) {i += 1;segment += input[i];}curByteLength += getLength(segment);if (curByteLength === maxLength) {return input.slice(0, i + 1);}if (curByteLength > maxLength) {return input.slice(0, i - segment.length + 1);}}return input;};/*** @param url {string}* @returns {Promise<string>}*/const toDataURL = (url) => fetch(url).then((response) => response.blob()).then((blob) => new Promise((resolve, reject) => {const reader = new FileReader();reader.onloadend = () => resolve(reader.r###lt);reader.onerror = reject;reader.readAsDataURL(blob);}));/*** @param base64Data {string}* @returns {Promise<string>}*/const WebpToJpg = function (base64Data) {return new Promise((resolve) => {const image = new Image();image.src = base64Data;image.onload = () => {const canvas = document.createElement('canvas');const context = canvas.getContext('2d');canvas.width = image.width;canvas.height = image.height;context.drawImage(image, 0, 0);resolve(canvas.toDataURL('image/jpeg'));};});};/*** Waits for an element satisfying selector to exist, then resolves promise with the element.* Useful for resolving race conditions.*//*** @param selector {string}* @returns {Promise<Element>}*/const elementReady = function (selector) {return new Promise((resolve) => {// Check if the element already existsconst element = document.querySelector(selector);if (element) {resolve(element);}// It doesn't so, so let's make a mutation observer and waitnew MutationObserver((mutationRecords, observer) => {// Query for elements matching the specified selectorArray.from(document.querySelectorAll(selector)).forEach((foundElement) => {// Resolve the element that we foundresolve(foundElement);// Once we have resolved we don't need the observer anymore.observer.disconnect();});}).observe(document.documentElement, { childList: true, subtree: true });});};class BaseTaoElement {constructor($element, data) {this.element = $element;this.data = data;// Set the order idthis.orderId = data.oid;// Item namethis.title = truncate(removeWhitespaces(data.goodsname), 255);// Item and shipping pricesthis.itemPrice = `CNY ${data.goodsprice}`;this.freightPrice = `CNY ${data.sendprice}`;// URL related stuffthis.url = data.goodsurl;this.website = determineWebsite(this.url);// QC images locationthis.qcImag###rl = `https://www.basetao.com/best-taobao-agent-service/purchase/order_img/${data.oid}.html`;// Item sizing (if any)let sizing = removeWhitespaces(data.goodssize);sizing = (sizing !== '-' && sizing.toLowerCase() !== 'no') ? truncate(sizing, 255) : '';this.sizing = sizing.length !== 0 ? sizing : null;// Item color (if any)let color = removeWhitespaces(data.goodscolor);color = (color !== '-' && color.toLowerCase() !== 'no') ? truncate(color, 255) : '';this.color = color.length !== 0 ? color : null;// Item weightconst weight = removeWhitespaces(data.orderweight);this.weight = weight.length !== 0 ? `${weight} gram` : null;// Image url storage, for laterthis.imageUrls = [];// Set at a later date, if everthis.albumId = null;}/*** @return {string}*/get albumUrl() {return `https://imgur.com/a/${this.albumId}`;}/*** @param imageUrls {string[]}*/set images(imageUrls) {this.imageUrls = imageUrls;}/*** @returns {string}*/get purchaseUrl() {return cleanPurchaseUrl(this.url, this.website);}}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';const Loading = 'data:image/gif;base64,R0lGODlhEAAQAPMAAP////r6+paWlr6+vnx8fIyMjOjo6NDQ0ISEhLa2tq6urvDw8MjIyODg4J6enqampiH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAEAAQAAAETBDISau9NQjCiUxDYGmdhBCFkRUlcLCFOA3oNgXsQG2HRh0EAYWDIU6MGSSAR1G4ghRa7KjIUXCog6QzpRhYiC1HILsOEuJxGcNuTyIAIfkECQoAAAAsAAAAABAAEAAABGIQSGkQmzjLQkTTWDAgRGmAgMGVhAIESxZwBUMgSyAUATYQPIBg8OIQJwLCQbJkdjAlUCA6KfU0VEmyGWgenpNfcCAoEo6SmWtBYtCukxhAwQKeQAYWYgAHNZIFKBoMCHcTEQAh+QQJCgAAACwAAAAAEAAQAAAEWhDIOZejGDNysgyDQBAIGWRGMa7jgAVq0TUj0lEDUZxArvAU0a1nAAQOrsnIA1gqCZ6AUzI4nAxJwIEgyAQUhCQsjDmUCI1jDEhlrQrFV+ksGLApWwYz41jsIwAh+QQJCgAAACwAAAAAEAAQAAAEThDISau9IIQahiCEMGhCQxkFqBLFZ0pBWhzSkYIvMLAb/OGTBII2+QExSEBjuexhVgrKAZGgqKKTGGFgBc00Np71cVsVDJVo5ydyJt/wCAAh+QQJCgAAACwAAAAAEAAQAAAEWhDISau9OAxBiBjBtRRdSRTGpRRHeJBFOKWALAXkAKQNoSwWBgFRQAA4Q5DkgOwwhCXBYTJAdAQAopVhWSgIjR1gcLLVQrQbrBV4CcwSA8l0Alo0yA8cw+9TIgAh+QQJCgAAACwAAAAAEAAQAAAEWhDISau9WA5CxAhWMDDAwXGFQR0IgQRgWRBF7JyEQgXzIC2MFkc1MQkonMbAhyQ0Y5pBg0MREA4UwwnBWGhoUIAC55DwaAcQrIXATgyzE/bwCQ2sBGZmz7dEAAA7';class Imgur {/*** @param version {string}* @param config {GM_config}* @param agent {string}* @constructor*/constructor(version, config, agent) {this.version = version;this.agent = agent;if (config.get('imgurApi') === 'imgur') {this.headers = {authorization: `Client-ID ${config.get('imgurClientId')}`,'Content-Type': 'application/json',};this.host = config.get('imgurApiHost');return;}if (config.get('imgurApi') === 'rapidApi') {this.headers = {authorization: `Bearer ${config.get('rapidApiBearer')}`,'x-rapidapi-key': config.get('rapidApiKey'),'x-rapidapi-host': config.get('rapidApiHost'),};this.host = config.get('rapidApiHost');return;}throw new Error('Invalid Imgur API has been chosen');}/*** @param options* @returns {Promise<*|null>}*/async CreateAlbum(options) {const requestData = {url: `https://${this.host}/3/album`,type: 'POST',headers: this.headers,data: JSON.stringify({title: options.title,}),};Sentry.addBreadcrumb({category: 'Imgur',message: 'Creating album',data: requestData,});Logger.debug('Creating album', requestData);return $.ajax(requestData).retry({ times: 3 }).catch((err) => {// Check if Imgur is being a bitchif (typeof err.responseJSON === 'undefined') {// Store request so we know what was askedthis._storeRequestError(err);throw new ImgurError('Could not make an album, because Imgur is returning empty responses. Please try again later...', err);}this._handleImgurError(err);});}/*** @param base64Image {string}* @param albumDeleteHash {string}* @param purchaseUrl {string}* @returns {Promise<boolean>}*/async AddBase64ImageToAlbum(base64Image, albumDeleteHash, purchaseUrl) {// First step, upload the imageconst requestData = {url: `https://${this.host}/3/image`,headers: this.headers,type: 'POST',data: JSON.stringify({album: albumDeleteHash,type: 'base64',image: base64Image,description: this._getImageDescription(purchaseUrl),}),};Logger.debug('Adding image to album', requestData);Sentry.addBreadcrumb({category: 'Imgur',message: 'Adding image to album',data: requestData,});await $.ajax(requestData).retry({ times: 3 }).catch((err) => {// Check if Imgur is being a bitchif (typeof err.responseJSON === 'undefined') {// Store request so we know what was askedthis._storeRequestError(err);}this._handleImgurError(err);});}/*** @param imageUrl {string}* @param albumDeleteHash {string}* @param purchaseUrl {string}* @returns {Promise<*|null>}*/async AddImageToAlbum(imageUrl, albumDeleteHash, purchaseUrl) {// First step, upload the imageconst requestData = {url: `https://${this.host}/3/image`,headers: this.headers,type: 'POST',data: JSON.stringify({album: albumDeleteHash,image: imageUrl,description: this._getImageDescription(purchaseUrl),}),};Logger.debug('Adding image to album', requestData);Sentry.addBreadcrumb({category: 'Imgur',message: 'Adding image to album',data: requestData,});await $.ajax(requestData).retry({ times: 3 }).catch((err) => {// Check if Imgur is being a bitchif (typeof err.responseJSON === 'undefined') {// Store request so we know what was askedthis._storeRequestError(err);}this._handleImgurError(err);});}/*** @param deleteHash {string}*/RemoveAlbum(deleteHash) {const requestData = {url: `https://${this.host}/3/album/${deleteHash}`,headers: this.headers,type: 'DELETE',};Sentry.addBreadcrumb({category: 'Imgur',message: 'Removing album',data: requestData,});$.ajax(requestData).retry({ times: 3 }).catch(() => {});}_getAlbumDescription() {return `Auto uploaded using FR:Reborn - ${this.agent} ${this.version}`;}/*** @param purchaseUrl {string}*/_getImageDescription(purchaseUrl) {return purchaseUrl.length === 0 ? this._getAlbumDescription() : `W2C: ${purchaseUrl}`;}/*** @private* @param err {Error}*/_storeRequestError(err) {Sentry.addBreadcrumb({category: 'Imgur',message: `Imgur returned: '${err.statusText}'`,data: err,level: Sentry.Severity.Error,});}/*** @private* @param err {Error}*/_handleImgurError(err) {// If there is a server error, let the user nowif (err.status === 503 || (err.responseJSON && err.responseJSON.status === 503)) {throw new ImgurError('Imgur is either down, over-capacity or you did too many requests. Try again later', err);}// If we uploaded too many files, re-throw as proper error (checking via old response setup, new response setup and a simple fallback)if (err.responseJSON && err.responseJSON.data && err.responseJSON.data.error && err.responseJSON.data.error.code === 429) {throw new ImgurSlowdownError(err.responseJSON.data.error.message, err);} else if (err.responseJSON && err.responseJSON.errors && err.responseJSON.errors.length === 1 && err.responseJSON.errors[0] && err.responseJSON.errors[0].code === 429) {throw new ImgurSlowdownError(err.responseJSON.errors[0].detail, err);} else if (err.status === 429 || (err.responseJSON && err.responseJSON.status === 429)) {throw new ImgurSlowdownError('Too Many Requests', err);}// Store request so we know what was askedthis._storeRequestError(err);// If we have error data from Imgur, throw it (checking via the old response setup and the new one)if (err.responseJSON && err.responseJSON.data && err.responseJSON.data.error) {throw new ImgurError(`An error happened when uploading the image:\n${err.responseJSON.data.error.message}`, err);} else if (err.responseJSON && err.responseJSON.errors && err.responseJSON.errors.length !== 0 && err.responseJSON.errors[0].detail) {throw new ImgurSlowdownError(`An error happened when uploading the image:\n${err.responseJSON.errors[0].detail}`, err);}// If not, just show the full JSONthrow new ImgurError(`An error happened when uploading the image:\n${err.responseJSON}`, err);}}const buildSwaggerHTTPError = function (response) {// Build basic error (and use response as extra)const error = new Error(`${response.body.detail}: ${response.url}`);// Add status and status codeerror.status = response.body.status;error.statusCode = response.body.status;return error;};class QC {/*** @param version {string}* @param client {SwaggerClient}* @param userHash {string}* @param identifier {string}* @param agent {string}*/constructor(version, client, userHash, identifier, agent) {this.version = version;this.client = client;this.userHash = userHash;this.identifier = identifier;this.agent = agent;}/*** @param element {BaseTaoElement|CSSBuyElement|WeGoBuyElement}* @returns {Promise<null|string>}*/existingAlbumByOrderId(element) {const request = { url: element.url, orderId: element.orderId };return this.client.apis.QualityControl.uploaded(request).then((response) => {if (typeof response.body === 'undefined') {return null;}if (!response.body.success) {return null;}// Force add the album ID to the elementelement.albumId = response.body.albumId; // eslint-disable-line no-param-reassignreturn response.body.albumId;}).catch((reason) => {Logger.error(`Could not check if the album for order '${element.orderId}' exists on the QC server`, reason);// For some reason we couldn't fetch information, just return, server probably down or somethingif (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {return '-1';}// Add breadcrumb with actual request we didSentry.addBreadcrumb({category: 'Swagger',message: 'existingAlbumByOrderId',data: { request },level: Sentry.Severity.Debug,});// Add breadcrumb with the errorSentry.addBreadcrumb({category: 'Swagger - Error',message: 'existingAlbumByOrderId',data: { error: reason },level: Sentry.Severity.Error,});// Swagger HTTP errorif (typeof reason.response !== 'undefined') {Sentry.captureException(buildSwaggerHTTPError(reason));return '-1';}Sentry.captureException(new Error(`Could not check if the album for order '${element.orderId}' exists on the QC server`));return '-1';});}/*** @param url {string}* @returns {Promise<boolean>}*/exists(url) {const request = { url };return this.client.apis.QualityControl.exists(request).then((response) => {if (typeof response.body === 'undefined') {return null;}if (!response.body.success) {return null;}return response.body.exists;}).catch((reason) => {Logger.error(`Could not check if any album exists on the QC server, URL: '${url}'`, reason);// For some reason we couldn't fetch information, just return, server probably down or somethingif (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource') || reason.message.includes('response status is 200')) {return '-1';}// Add breadcrumb with actual request we didSentry.addBreadcrumb({category: 'Swagger',message: 'exists',data: { request },level: Sentry.Severity.Debug,});// Add breadcrumb with the errorSentry.addBreadcrumb({category: 'Swagger - Error',message: 'exists',data: { error: reason },level: Sentry.Severity.Error,});// Swagger HTTP errorif (typeof reason.response !== 'undefined') {Sentry.captureException(buildSwaggerHTTPError(reason));return false;}Sentry.captureException(new Error(`Could not check if any album exists on the QC server, URL: '${url}'`));return false;});}/*** @param element {BaseTaoElement|CSSBuyElement|WeGoBuyElement}* @param album {string}*/uploadQc(element, album) {const request = {method: 'post',requestContentType: 'application/json',requestBody: {usernameHash: this.userHash,identifier: this.identifier,albumId: album,color: element.color,orderId: element.orderId,purchaseUrl: element.purchaseUrl,sizing: element.sizing,itemPrice: element.itemPrice,freightPrice: element.freightPrice,weight: element.weight,source: `${this.agent} to Imgur ${this.version}`,website: element.website,},};Logger.log('Adding new QC to FR: Reborn', request);return this.client.apis.QualityControl.postQualityControlCollection({}, request).catch((reason) => {Logger.error('Could not upload QC to the QC server', reason);// For some reason we couldn't fetch information, just return, server probably down or somethingif (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {return;}// If the order already exists, just ignore the errorif (reason.message.includes('orderId: This value is already used')) {return;}// Add breadcrumb with actual request we didSentry.addBreadcrumb({category: 'Swagger',message: 'postQualityControlCollection',data: { request, element },level: Sentry.Severity.Debug,});// Add breadcrumb with the errorSentry.addBreadcrumb({category: 'Swagger - Error',message: 'postQualityControlCollection',data: { error: reason },level: Sentry.Severity.Error,});// Swagger HTTP errorif (typeof reason.response !== 'undefined') {Sentry.captureException(buildSwaggerHTTPError(reason.response));return;}Sentry.captureException(new Error('Could not upload QC to the QC server'));});}}class BaseTao {constructor() {this.setup = false;}/*** @param hostname {string}* @returns {boolean}*/supports(hostname) {return hostname.includes('basetao.com');}/*** @returns {string}*/name() {return 'BaseTao';}/*** @param client {Promise<SwaggerClient>}* @returns {Promise<BaseTao>}*/async build(client) {// If already build before, just returnif (this.setup) {return this;}// Get the usernamelet username = $('[aria-labelledby="profileDropdown"] a:first').text();if (typeof username === 'undefined' || username == null || username === '') {Snackbar('You need to be logged in to use this extension.');throw new Error('You need to be logged in to use this extension.');}// Trim the usernameusername = username.trim();// Hash the username (and add an extra space, since this was a bug in the past)const userHash = SparkMD5.hash(`${username} `);// Ensure we know who triggered errorsSentry.setUser({ id: userHash, username });// Build all the clientsthis.imgurClient = new Imgur(GM_info.script.version, GM_config, this.name());this.qcClient = new QC(GM_info.script.version, await client, userHash, username, this.name());// Mark that this agent has been set upthis.setup = true;return this;}/*** @return {Promise<void>}*/async process() {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}// Make copy of the current this, so we can use it laterconst agent = this;// Get the container (unsafe, because we want the actual jQuery table)const $container = window.unsafeWindow.jQuery('#table').first();// Start processing once the table has been loadedelementReady('.details-tr i.bi.bi-image-fill').then(() => {const rowData = $container.bootstrapTable('getData');$container.find('i.bi.bi-image-fill').each(function () {const $element = $(this);const orderId = $element.parents('td').find('[data-row]').data('row');agent._buildElement($element, rowData.find(agent._getRowData(orderId)));});$container.on('load-success.bs.table', (event, data) => {$container.find('i.bi.bi-image-fill').each(function () {const $element = $(this);const orderId = $element.parents('td').find('[data-row]').data('row');agent._buildElement($(this), data.rows.find(agent._getRowData(orderId)));});});// Ensure tooltips$container.tooltip({ selector: '.qc-tooltip' });});}/*** @private* @param $this* @param {RowData} data* @return {Promise<BaseTaoElement>}*/async _buildElement($this, data) {const element = new BaseTaoElement($this, data);const $imageIcon = element.element.parents('a').first();// Append download button if enabledif (GM_config.get('showImagesDownloadButton')) {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>');$download.on('click', () => this._downloadHandler($download, element));$imageIcon.parent().append($download);}// This plugin only works for certain websites, so check if element is supportedif (element.website === WEBSITE_UNKNOWN) {const $upload = $(`<div><span style="cursor: pointer;"><img src="${ImgurIcon}" alt="Create a basic album"></span></div>`);$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>'));$upload.on('click', () => {this._uploadHandler(element);});$this.parents('td').first().append($upload);return element;}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>`);$this.parents('td').first().append($loading);// Define upload objectconst $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>`);// If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.const albumId = await this.qcClient.existingAlbumByOrderId(element);if (albumId === '-1') {$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>'));$this.parents('td').first().append($upload);$loading.remove();return element;}// Have you ever uploaded a QC? If so, link to that albumconst $image = $upload.find('img');if (albumId !== null && albumId !== '-1') {$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>'));$image.wrap(`<a href='${element.albumUrl}' target='_blank' data-bs-toggle="tooltip" data-bs-placement="bottom" title='Go to album'></a>`);$image.removeAttr('title');$this.parents('td').first().append($upload);$loading.remove();return element;}// Has anyone ever uploaded a QC, if not, show a red markerconst exists = await this.qcClient.exists(element.purchaseUrl);if (!exists) {$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>'));$upload.on('click', () => {this._uploadHandler(element);});$this.parents('td').first().append($upload);$loading.remove();return element;}// A previous QC exists, but you haven't uploaded yours yet, show orange marker$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>'));$upload.on('click', () => {this._uploadHandler(element);});$this.parents('td').first().append($upload);$loading.remove();return element;}/*** @private* @param element {BaseTaoElement}* @returns {Promise<void>}*/async _uploadToImgur(element) {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}if (element.imageUrls.length === 0) {Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);return;}const $processing = $(`<span style="cursor: wait;"><img src="${Loading}" alt="Processing..."></span>`);const $base = element.element.parents('td').first().find('div').first();$base.after($processing).hide();// Start the processSnackbar(`Pictures for '${element.orderId}' are being uploaded...`);// Temp store deleteHashlet deleteHash;try {// Create the albumconst response = await this.imgurClient.CreateAlbum(element);if (typeof response === 'undefined' || response == null) {return;}// Extract and build information neededdeleteHash = response.data.deletehash;const albumId = response.data.id;// Upload all QC imagesconst promises = [];$.each(element.imageUrls, (key, imageUrl) => {// Convert to base64, since Imgur cannot access our imagespromises.push(toDataURL(imageUrl).then(async (data) => {// Store our base64 and if the file is WEBP, convert it to JPGlet base64Image = data;if (base64Image.indexOf('image/webp') !== -1) {base64Image = await WebpToJpg(base64Image);}// Remove the unnecessary `data:` partconst cleanedData = base64Image.replace(/(data:image\/.*;base64,)/, '');// Upload the image to the albumreturn this.imgurClient.AddBase64ImageToAlbum(cleanedData, deleteHash, element.purchaseUrl);}));});// Wait until everything has been tried to be uploadedawait Promise.all(promises);// Set albumId in element, so we don't upload it again (when doing a pending haul upload)element.albumId = albumId; // eslint-disable-line no-param-reassign// Tell the user it was uploaded and open the album in the backgroundSnackbar('Pictures have been uploaded!', 'success');GM_openInTab(element.albumUrl, true);// Tell QC Suite about our uploaded QC's (if it's supported)if (element.website !== WEBSITE_UNKNOWN) {this.qcClient.uploadQc(element, albumId);}// Wrap the logo in a href to the new albumconst $image = $base.find('img');$image.wrap(`<a href='${element.albumUrl}' target='_blank' data-bs-toggle="tooltip" data-bs-placement="bottom" title='Go to album'></a>`);$image.removeAttr('title');// Remove processing$processing.remove();// Update the markerconst checkMarkMessage = element.website !== WEBSITE_UNKNOWN ? 'You have uploaded a QC' : 'Album has been created';const $qcMarker = $base.find('.qc-marker:not(:first-child)').first();$qcMarker.attr('title', checkMarkMessage).css('cursor', 'help').css('color', 'green').text('✓');// Remove the click handler$base.off();// Show it again$base.show();} catch (err) {// Remove the created albumthis.imgurClient.RemoveAlbum(deleteHash);// Reset the button$processing.remove();$base.show();// Show the errorSnackbar(err.message, 'error');// If it's the slow down error, don't log itif (err instanceof ImgurSlowdownError) {return;}// Log the errorSentry.captureException(err);Logger.error(err);}}/*** @private* @param $download* @param element {BaseTaoElement}*/async _downloadHandler($download, element) {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}if (!await ConfirmDialog()) {return;}// Remove button so people don't do dumb shit$download.remove();// Go to the QC pictures URL, grab all image sources and upload the elementawait $.get(element.qcImag###rl).then(async (data) => {if (data.indexOf('long time no operation ,please sign in again') !== -1) {Snackbar('You are no longer logged in, reloading page....', 'warning');Logger.info('No longer logged in, reloading page for user...');window.location.reload();return null;}Snackbar('Zipping images, this might take a while....', 'info');// Create a zip file writerconst zipWriter = new zip.ZipWriter(new zip.Data64URIWriter('application/zip'));// Download all the images and add to the zipconst promises = [];$('<div/>').html(data).find('.card > img').each(function () {const src = $(this).attr('src');promises.push(new Promise((resolve) => toDataURL(src).then((dataURI) => zipWriter.add(src.substring(src.lastIndexOf('/') + 1), new zip.Data64URIReader(dataURI))).then(() => resolve())));});// Wait for all images to be added to the ZIPawait Promise.all(promises);// Close the ZipWriter object and download to computersaveAs(await zipWriter.close(), `${element.orderId}.zip`);Snackbar(`Downloading ${element.orderId}.zip`, 'success');return null;}).catch((err) => {Snackbar(`Could not get all images for order ${element.orderId}`);Logger.error(`Could not get all images for order ${element.orderId}`, err);});}/*** @private* @param element {BaseTaoElement}* @returns {Promise<void>}*/async _uploadHandler(element) {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}// Go to the QC pictures URL, grab all image sources and upload the elementawait $.get(element.qcImag###rl).then(async (data) => {if (data.indexOf('long time no operation ,please sign in again') !== -1) {Snackbar('You are no longer logged in, reloading page....', 'warning');Logger.info('No longer logged in, reloading page for user...');window.location.reload();return null;}// Add all image urls to the element$('<div/>').html(data).find('main div.container.container img').each(function () {element.imageUrls.push($(this).attr('src'));});// Finally go and upload the orderreturn this._uploadToImgur(element);}).catch((err) => {Snackbar(`Could not get all images for order ${element.orderId}`);Logger.error(`Could not get all images for order ${element.orderId}`, err);});}/*** @private* @param orderId*/_getRowData(orderId) {return (item) => Number(item.oid) === Number(orderId);}}class CSSBuyElement {constructor($element) {this.element = $element;// Create empty array for imagesthis.imageUrls = [];// Temporary itemsconst parentTableEntry = $element.parentsUntil('tbody');const itemLink = parentTableEntry.find('td.tabletd3 > a');const splitText = parentTableEntry.find('td.tabletd3 > span:nth-child(3)').html().split('<br>');// Order detailsthis.orderId = this.element.parent().attr('data-id');// Item namethis.title = truncate(removeWhitespaces(itemLink.text()), 255);// Purchase detailsthis.website = determineWebsite(itemLink.attr('href'));this.purchaseUrl = cleanPurchaseUrl(itemLink.attr('href'), this.website);// Item pricethis.itemPrice = `CNY ${removeWhitespaces(parentTableEntry.find('td:nth-child(4) > span:nth-child(1)').text())}`;// Freight pricethis.freightPrice = `CNY ${removeWhitespaces(parentTableEntry.find('td:nth-child(5) span').text())}`;// Item weightconst weight = removeWhitespaces(parentTableEntry.find('td:nth-child(7) span').text());this.weight = weight.length !== 0 ? `${weight} gram` : null;// Item sizing and color (if any)this.color = null;this.sizing = null;try {if (splitText.length === 1) {let color = splitText[0].split(' : ')[1];color = (typeof color !== 'undefined' && color !== '-' && color.toLowerCase() !== 'no') ? truncate(color, 255) : '';this.color = color.length !== 0 ? color : null;} else if (splitText.length === 2) {let sizing = (splitText[0].split(' : ')[1]);sizing = (typeof sizing !== 'undefined' && sizing !== '-' && sizing.toLowerCase() !== 'no') ? truncate(sizing, 255) : '';this.sizing = sizing.length !== 0 ? sizing : null;let color = (splitText[1].split(' : ')[1]);color = (typeof color !== 'undefined' && color !== '-' && color.toLowerCase() !== 'no') ? truncate(color, 255) : '';this.color = color.length !== 0 ? color : null;} else if (splitText.length !== 0) {this.sizing = splitText.join('\n');}} catch (e) {Logger.info('Could not figure out sizing/color', e);}// Set at a later date, if everthis.albumId = null;}/*** @return {string}*/get albumUrl() {return `https://imgur.com/a/${this.albumId}`;}}/* eslint-disable no-return-await */class OSS {constructor() {this.setup = false;this.window = window.unsafeWindow;}async build() {if (this.setup) {return this;}// Try and build the OSS clienttry {// Grab the OSS clientconst WindowOSS = await this._waitForValue('OSS');// Build the config for the bucketconst config = {region: await this._waitForValue('c_region'),accessKeyId: await this._waitForValue('c_accessid'),accessKeySecret: await this._waitForValue('c_accesskey'),bucket: await this._waitForValue('c_bucket'),endpoint: `https://${await this._waitForValue('c_region')}.aliyuncs.com/`,};// Log the config, for ease of useLogger.info('OSS config build', config);// Set up the bucket for easy usethis.window.client = new WindowOSS.Wrapper(config);// Mark as readythis.setup = true;} catch (e) {throw new Error(e);}return this;}/*** @param {string} orderId** @return Promise<object>*/async list(orderId) {if (this.setup === false) {throw new Error('OSS is not setup, so cannot be used');}return await this.window.client.list({'max-keys': 100,prefix: `o/${orderId}/`,});}_waitForValue(value) {return new Promise((resolve) => {// Check if the element already existsif (this.window[value]) {resolve(this.window[value]);return;}const _waitForGlobal = () => {if (this.window[value]) {resolve(this.window[value]);return;}// Wait until we have itsetTimeout(() => { _waitForGlobal(value, resolve); }, 100);};// It doesn't so, so let's start waiting for it_waitForGlobal(value, resolve);});}}class CSSBuy {constructor() {this.setup = false;}/*** @param hostname {string}* @returns {boolean}*/supports(hostname) {return hostname.includes('cssbuy.com');}/*** @returns {string}*/name() {return 'CSSBuy';}/*** @param client {Promise<SwaggerClient>}* @returns {Promise<CSSBuy>}*/async build(client) {// If already build before, just returnif (this.setup) {return this;}// Get the usernameconst username = removeWhitespaces($(await $.get('/?go=m')).find('.userxinix > div:nth-child(1) > p').text());if (typeof username === 'undefined' || username == null || username === '') {Snackbar('You need to be logged in to use this extension.');return this;}// Ensure we know who triggered the errorconst userHash = SparkMD5.hash(username);Sentry.setUser({ id: userHash, username });// Build all the clientsthis.imgurClient = new Imgur(GM_info.script.version, GM_config, this.name());this.qcClient = new QC(GM_info.script.version, await client, userHash, username, this.name());// Mark that this agent has been set upthis.setup = true;return this;}async process() {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}// Make copy of the current this, so we can use it laterconst agent = this;// If there is nothing to process, just return here (so we don't try to build the OSS client and die)const $elements = $(".oss-photo-view-button > a:contains('QC PIC')");if ($elements.length === 0) {return;}// Build OSS clientthis.ossClient = new OSS();await this.ossClient.build();if (this.ossClient.setup === false) {Snackbar('Could not build the OSS client, check the console for errors.');}// Add icons to all elements$elements.each(function () { agent._buildElement($(this)); });}/*** @private* @param $this* @return {Promise<void>}*/async _buildElement($this) {const element = new CSSBuyElement($this);// Check if it has any images to begin withconst r###lt = await this.ossClient.list(element.orderId);if (typeof r###lt.objects === 'undefined') {return;}// This plugin only works for certain websites, so check if element is supportedif (element.website === WEBSITE_UNKNOWN) {const $upload = $(`<ul class="badge-lists"><li style="cursor: pointer"><img src="${ImgurIcon}" alt="Create a basic album" style="width: 100%"></li></ul>`);$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>'));$upload.on('click', () => { this._uploadToImgur(element); });$this.parents('ul').first().after($upload);return;}// Define column in which to show buttonsconst $other = $this.parents('ul').first();// Show simple loading animationconst $loading = $(`<ul class="badge-lists"><li style="cursor: wait"><img src="${Loading}" alt="Loading..." style="width: 100%"></li></ul>`);$other.after($loading);// Define upload objectconst $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>`);// If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.const albumId = await this.qcClient.existingAlbumByOrderId(element);if (albumId === '-1') {$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>'));$upload.on('click', () => { this._uploadToImgur(element); });$other.after($upload);$loading.remove();return;}// Have you ever uploaded a QC? If so, link to that albumconst $image = $upload.find('img');if (albumId !== null && albumId !== '-1') {$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>'));$image.wrap(`<a href='https://imgur.com/a/${albumId}' target='_blank' title='Go to album'></a>`);$image.removeAttr('title');$other.after($upload);$loading.remove();return;}// Has anyone ever uploaded a QC, if not, show a red markerconst exists = await this.qcClient.exists(element.purchaseUrl);if (!exists) {$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>'));$upload.on('click', () => { this._uploadToImgur(element); });$other.after($upload);$loading.remove();return;}// A previous QC exists, but you haven't uploaded yours yet, show orange marker$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>'));$upload.on('click', () => { this._uploadToImgur(element); });$other.after($upload);$loading.remove();}/*** @param element {CSSBuyElement}* @returns {Promise<void>}*/async _uploadToImgur(element) {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}const $processing = $(`<ul class="badge-lists"><li style="cursor: wait"><img src="${Loading}" alt="Processing..." style="width: 100%"></li></ul>`);const $base = element.element.parents('td').first().find('.badge-lists');$base.after($processing).hide();const r###lt = await this.ossClient.list(element.orderId);if (typeof r###lt.objects === 'undefined') {Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);return;}r###lt.objects.forEach((item) => {element.imageUrls.push((item.url));});if (element.imageUrls.length === 0) {Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);return;}// Start the processSnackbar(`Pictures for '${element.orderId}' are being uploaded...`);// Temp store deleteHashlet deleteHash;try {// Create the albumconst response = await this.imgurClient.CreateAlbum(element);if (typeof response === 'undefined' || response == null) {return;}// Extract and build information neededdeleteHash = response.data.deletehash;const albumId = response.data.id;// Upload all QC imagesconst promises = [];$.each(element.imageUrls, (key, imageUrl) => {promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl));});// Wait until everything has been tried to be uploadedawait Promise.all(promises);// Set albumId in element, so we don't upload it again (when doing a pending haul upload)element.albumId = albumId; // eslint-disable-line no-param-reassign// Tell the user it was uploaded and open the album in the backgroundSnackbar('Pictures have been uploaded!', 'success');GM_openInTab(element.albumUrl, true);// Tell QC Suite about our uploaded QC's (if it's supported)if (element.website !== WEBSITE_UNKNOWN) {this.qcClient.uploadQc(element, albumId);}// Wrap the logo in a href to the new albumconst $image = $base.find('img');$image.wrap(`<a href='${element.albumUrl}' target='_blank' title='Go to album'></a>`);$image.removeAttr('title');// Remove processing$processing.remove();// Update the markerconst checkMarkMessage = element.website !== WEBSITE_UNKNOWN ? 'You have uploaded a QC' : 'Album has been created';const $qcMarker = $base.find('.qc-marker:not(:first-child)').first();$qcMarker.attr('title', checkMarkMessage).css('cursor', 'help').css('color', 'green').text('✓');// Remove the click handler$base.off();// Show it again$base.show();} catch (err) {// Remove the created albumthis.imgurClient.RemoveAlbum(deleteHash);// Reset the button$processing.remove();$base.show();// Show the errorSnackbar(err.message, 'error');// If it's the slow down error, don't log itif (err instanceof ImgurSlowdownError) {return;}// Log the errorSentry.captureException(err);Logger.error(err);}}}class WeGoBuyElement {constructor($element) {this.element = $element;// Order detailsthis.orderId = removeWhitespaces($element.find('td:nth-child(1) > p').text());this.imageUrls = $element.find('.lookPic').map((key, value) => $(value).attr('href')).get();// Item namethis.title = truncate(removeWhitespaces($element.find('.js-item-title').text()), 255);// Item sizing (if any)const sizing = removeWhitespaces($element.find('.user_orderlist_txt').text());this.sizing = sizing.length !== 0 ? truncate(sizing, 255) : null;// Item color (WeGoBuy doesn't support separation of color, so just null)this.color = null;// Item priceconst itemPriceMatches = truncate(removeWhitespaces($element.parents('tbody').find('tr > td:nth-child(2)').text()), 255).match(/([A-Z]{2})\W+([.,0-9]+)/);this.itemPrice = `${itemPriceMatches[1]} ${itemPriceMatches[2]}`;// Freight priceconst 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]+)/);this.freightPrice = `${freightPriceMatches[1]} ${freightPriceMatches[2]}`;// Item weightthis.weight = null;// Purchase detailsconst possibleUrl = removeWhitespaces($element.find('.js-item-title').attr('href')).trim();this.url = isUrl(possibleUrl) ? possibleUrl : '';this.website = determineWebsite(this.url);// Set at a later date, if everthis.albumId = null;}/*** @return {string}*/get albumUrl() {return `https://imgur.com/a/${this.albumId}`;}/*** @param imageUrls {string[]}*/set images(imageUrls) {this.imageUrls = imageUrls;}/*** @returns {string}*/get purchaseUrl() {return cleanPurchaseUrl(this.url, this.website);}}class WeGoBuy {constructor() {this.setup = false;}/*** @param hostname {string}* @returns {boolean}*/supports(hostname) {return hostname.includes('wegobuy.com')|| hostname.includes('superbuy.com');}/*** @returns {string}*/name() {return 'WeGoBuy';}/*** @param client {Promise<SwaggerClient>}* @returns {Promise<WeGoBuy>}*/async build(client) {// If already build before, just returnif (this.setup) {return this;}// Ensure the toast looks decent on SB/WGBGM_addStyle('.swal2-popup.swal2-toast .swal2-title {font-size: 1em; font-weight: bolder}');// Get the usernameconst username = (await $.get('/ajax/user-info')).data.user_name;if (typeof username === 'undefined' || username == null || username === '') {Snackbar('You need to be logged in to use this extension.');return this;}// Ensure we know who triggered the errorconst userHash = SparkMD5.hash(username);Sentry.setUser({ id: userHash, username });// Build all the clientsthis.imgurClient = new Imgur(GM_info.script.version, GM_config, this.name());this.qcClient = new QC(GM_info.script.version, await client, userHash, username, this.name());// Mark that this agent has been setupthis.setup = true;return this;}async process() {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}// Make copy of the current this, so we can use it laterconst agent = this;// Add icons to all elements$('.pic-list.j_picList').each(function () {agent._buildElement($(this).parents('tr'));});}/*** @private* @param $this* @return {Promise<void>}*/async _buildElement($this) {const element = new WeGoBuyElement($this);// No pictures (like rehearsal orders), no QC optionsif (element.imageUrls.length === 0) {return;}// Define column in which to download buttonconst $inspection = $this.find('td:nth-child(6)').first();// Append download button if enabledif (GM_config.get('showImagesDownloadButton')) {const $download = $('<div style="padding:5px;"><p><span style="color: rgb(255, 140, 60);cursor: pointer;margin-top: 5px;">Download</span></p></div>');$download.on('click', () => this._downloadHandler($download, element));$inspection.append($download);}// This plugin only works for certain websites, so check if element is supportedif (element.website === WEBSITE_UNKNOWN || element.url.length === 0) {const $upload = $(`<div style="padding:5px;"><span style="cursor: pointer;"><img src="${ImgurIcon}" alt="Create a basic album"></span></div>`);$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>'));$upload.on('click', () => {this._uploadToImgur(element);});$inspection.append($upload);return;}// Show simple loading animationconst $loading = $(`<div style="padding:5px;"><span style="cursor: wait;"><img src="${Loading}" alt="Loading..."></span></div>`);$inspection.append($loading);// Define upload objectconst $upload = $(`<div style="padding:5px;"><span class="qc-marker" style="cursor: pointer;"><img src="${ImgurIcon}" alt="Upload your QC"></span></div>`);// If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.const albumId = await this.qcClient.existingAlbumByOrderId(element);if (albumId === '-1') {$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>'));$upload.on('click', () => {this._uploadToImgur(element);});$inspection.append($upload);$loading.remove();return;}// Have you ever uploaded a QC? If so, link to that albumconst $image = $upload.find('img');if (albumId !== null && albumId !== '-1') {$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>'));$image.wrap(`<a href='https://imgur.com/a/${albumId}' target='_blank' title='Go to album' style="display: initial;"></a>`);$image.removeAttr('title');$inspection.append($upload);$loading.remove();return;}// Has anyone ever uploaded a QC, if not, show a red markerconst exists = await this.qcClient.exists(element.purchaseUrl);if (!exists) {$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>'));$upload.on('click', () => {this._uploadToImgur(element);});$inspection.append($upload);$loading.remove();return;}// A previous QC exists, but you haven't uploaded yours yet, show orange marker$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>'));$upload.on('click', () => {this._uploadToImgur(element);});$inspection.append($upload);$loading.remove();}/*** @private* @param $download* @param element {WeGoBuyElement}*/async _downloadHandler($download, element) {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}if (!await ConfirmDialog()) {return;}// Remove button so people don't do dumb shit$download.remove();Snackbar('Zipping images, this might take a while....', 'info');// Create a zip file writerconst zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'));// Download all the images and add to the zipconst promises = [];$.each(element.imageUrls, (key, imageUrl) => {promises.push(toDataURL(imageUrl.replace('http://', 'https://')).then((dataURI) => zipWriter.add(imageUrl.substring(imageUrl.lastIndexOf('/') + 1), new zip.Data64URIReader(dataURI))));});// Wait for all images to be added to the ZIPawait Promise.all(promises);// Close the ZipWriter object and download to computersaveAs(await zipWriter.close(), `${element.orderId}.zip`);Snackbar(`Downloading ${element.orderId}.zip`, 'success');}/*** @param element {WeGoBuyElement}* @returns {Promise<void>}*/async _uploadToImgur(element) {if (this.setup === false) {throw new Error('Agent is not setup, so cannot be used');}const $processing = $(`<div style="padding:5px;"><span style="cursor: wait;"><img src="${Loading}" alt="Processing..."></span></div>`);const $options = element.element.find('td:nth-child(6)').first();const $base = $options.find('div').last();$base.after($processing).hide();// Start the processSnackbar(`Pictures for '${element.orderId}' are being uploaded...`);// Temp store deleteHashlet deleteHash;try {// Create the albumconst response = await this.imgurClient.CreateAlbum(element);if (typeof response === 'undefined' || response == null) {return;}// Extract and build information neededdeleteHash = response.data.deletehash;const albumId = response.data.id;// Upload all QC imagesconst promises = [];$.each(element.imageUrls, (key, imageUrl) => {promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl));});// Wait until everything has been tried to be uploadedawait Promise.all(promises);// Set albumId in element, so we don't upload it again (when doing a pending haul upload)element.albumId = albumId; // eslint-disable-line no-param-reassign// Tell the user it was uploaded and open the album in the backgroundSnackbar('Pictures have been uploaded!', 'success');GM_openInTab(element.albumUrl, true);// Tell QC Suite about our uploaded QC's (if it's supported)if (element.website !== WEBSITE_UNKNOWN) {this.qcClient.uploadQc(element, albumId);}// Remove processing$processing.remove();$base.remove();// Add new buttonsconst checkMarkMessage = element.website !== WEBSITE_UNKNOWN ? 'You have uploaded a QC' : 'Album has been created';$options.append($('<div style="padding:5px;">'+ `<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>`+ `<span class="qc-marker" style="cursor:help;margin-left:5px;color:green;font-weight: bold;" title="${checkMarkMessage}">✓</span>`+ '</div>'));// Remove the click handler$base.off();// Show it again$base.show();} catch (err) {// Remove the created albumthis.imgurClient.RemoveAlbum(deleteHash);// Reset the button$processing.remove();$base.show();// Show the errorSnackbar(err.message, 'error');// If it's the slow down error, don't log itif (err instanceof ImgurSlowdownError) {return;}// Log the errorSentry.captureException(err);Logger.error(err);}}}/*** @param hostname {string}** @returns {BaseTao|CSSBuy|WeGoBuy|null}*/function getAgent(hostname) {const agents = [new BaseTao(), new CSSBuy(), new WeGoBuy()];let agent = null;Object.values(agents).forEach((value) => {if (agent == null && value.supports(hostname)) {agent = value;}});return agent;}// Inject snackbar css styleGM_addStyle(GM_getResourceText('sweetalert2'));// Setup proper settings menuGM_config.init('Settings', {serverSection: {label: 'QC Server settings',type: 'section',},swaggerDocUrl: {label: 'Swagger documentation URL',type: 'text',default: 'https://www.fashionreps.page/api/doc.json',},generalSection: {label: 'General options',type: 'section',},showImagesDownloadButton: {label: 'Show the images download button/text',type: 'checkbox',default: 'true',},uploadSection: {label: 'Upload API Options',type: 'section',},imgurApi: {label: 'Select your Imgur API',type: 'radio',default: 'imgur',options: {imgur: 'Imgur API (Free)',rapidApi: 'RapidAPI (Freemium)',},},imgurSection: {label: 'Imgur Options',type: 'section',},imgurApiHost: {label: 'Imgur host',type: 'text',default: 'api.imgur.com',},imgurClientId: {label: 'Imgur Client-ID',type: 'text',default: 'e4e18b5ab582b4c',},rapidApiSection: {label: 'RadidAPI Options',type: 'section',},rapidApiHost: {label: 'RapidAPI host',type: 'text',default: 'imgur-apiv3.p.rapidapi.com',},rapidApiKey: {label: 'RapidAPI key (only needed if RapidApi select above)',type: 'text',default: '',},rapidApiBearer: {label: 'RapidAPI access token (only needed if RapidApi select above)',type: 'text',default: '',},});// Reload page if config changedGM_config.onclose = (saveFlag) => {if (saveFlag) {window.location.reload();}};// Register menu within GMGM_registerMenuCommand('Settings', GM_config.open);// Setup Sentry for proper error logging, which will make my lives 20x easier, use XHR since fetch dies.Sentry.init({dsn: 'https://[email protected]/5802425',tunnel: 'https://www.fashionreps.page/sentry/tunnel',transport: Sentry.Transports.XHRTransport,release: GM_info.script.version,defaultIntegrations: false,integrations: [new Sentry.Integrations.InboundFilters(),new Sentry.Integrations.FunctionToString(),new Sentry.Integrations.LinkedErrors(),new Sentry.Integrations.UserAgent(),],environment: 'production',normalizeDepth: 5,});// eslint-disable-next-line func-names(async function () {// Setup the logger.Logger.useDefaults();// Log the start of the script.Logger.info(`Starting extension '${GM_info.script.name}', version ${GM_info.script.version}`);// Get the proper agent, if anyconst agent = getAgent(window.location.hostname);if (agent === null) {Sentry.captureMessage(`Unsupported website ${window.location.hostname}`);Logger.error('Unsupported website');return;}Logger.info(`Agent '${agent.name()}' detected`);// Finally, try to build the proper agent and process the pagetry {await agent.build(new SwaggerClient({ url: GM_config.get('swaggerDocUrl') }));await agent.process();} catch (error) {if (error.message.includes('Failed to fetch') || error.message.includes('attempting to fetch resource')) {Snackbar('We are unable to connect to FR:Reborn, features will be disabled.');Logger.error(`We are unable to connect to FR:Reborn: ${GM_config.get('swaggerDocUrl')}`, error);return;}Snackbar(`An unknown issue has occurred when trying to setup the agent extension: ${error.message}`);Logger.error('An unknown issue has occurred', error);}}());