- // ==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://gf.qytechs.cn/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/sweetalert2@11.10.6/dist/sweetalert2.js
- // @require https://unpkg.com/js-logger@1.6.1/src/logger.js
- // @require https://unpkg.com/spark-md5@3.0.2/spark-md5.js
- // @require https://unpkg.com/@zip.js/zip.js@2.3.15/dist/zip-full.js
- // @require https://unpkg.com/file-saver@2.0.5/dist/FileSaver.js
- // @require https://unpkg.com/jquery@3.6.0/dist/jquery.js
- // @require https://unpkg.com/jquery.ajax-retry@0.2.7/src/jquery.ajax-retry.js
- // @require https://unpkg.com/@sentry/browser@6.19.7/build/bundle.js
- // @require https://unpkg.com/@sentry/tracing@6.19.7/build/bundle.tracing.js
- // @require https://unpkg.com/swagger-client@3.26.0/dist/swagger-client.browser.js
- // @require https://gf.qytechs.cn/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657
- // @resource sweetalert2 https://unpkg.com/sweetalert2@11.0.15/dist/sweetalert2.min.css
- // @run-at document-end
- // @icon https://i.imgur.com/mYBHjAg.png
- // ==/UserScript==
-
- // Define default toast
- const 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.resumeTimer);
- },
- });
-
- /**
- * @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((result) => resolve(result.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 websites
- const 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 URL
- const encodedURL = new URL(url).searchParams.get('url') || '';
- if (encodedURL.length === 0) {
- return url;
- }
-
- // Decode said encoded URL
- const 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 up
- return 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 first
- if (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 surrogate
- if (isLowSurrogate(codePoint)) {
- // when parsing previous hi-surrogate, 3 is added to byteLength
- if (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.result);
- 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 exists
- const element = document.querySelector(selector);
- if (element) {
- resolve(element);
- }
-
- // It doesn't so, so let's make a mutation observer and wait
- new MutationObserver((mutationRecords, observer) => {
- // Query for elements matching the specified selector
- Array.from(document.querySelectorAll(selector)).forEach((foundElement) => {
- // Resolve the element that we found
- resolve(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 id
- this.orderId = data.oid;
-
- // Item name
- this.title = truncate(removeWhitespaces(data.goodsname), 255);
-
- // Item and shipping prices
- this.itemPrice = `CNY ${data.goodsprice}`;
- this.freightPrice = `CNY ${data.sendprice}`;
-
- // URL related stuff
- this.url = data.goodsurl;
- this.website = determineWebsite(this.url);
-
- // QC images location
- this.qcImagesUrl = `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 weight
- const weight = removeWhitespaces(data.orderweight);
- this.weight = weight.length !== 0 ? `${weight} gram` : null;
-
- // Image url storage, for later
- this.imageUrls = [];
-
- // Set at a later date, if ever
- this.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 bitch
- if (typeof err.responseJSON === 'undefined') {
- // Store request so we know what was asked
- this._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 image
- const 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 bitch
- if (typeof err.responseJSON === 'undefined') {
- // Store request so we know what was asked
- this._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 image
- const 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 bitch
- if (typeof err.responseJSON === 'undefined') {
- // Store request so we know what was asked
- this._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 now
- if (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 asked
- this._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 JSON
- throw 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 code
- error.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 element
- element.albumId = response.body.albumId; // eslint-disable-line no-param-reassign
-
- return 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 something
- if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {
- return '-1';
- }
-
- // Add breadcrumb with actual request we did
- Sentry.addBreadcrumb({
- category: 'Swagger',
- message: 'existingAlbumByOrderId',
- data: { request },
- level: Sentry.Severity.Debug,
- });
-
- // Add breadcrumb with the error
- Sentry.addBreadcrumb({
- category: 'Swagger - Error',
- message: 'existingAlbumByOrderId',
- data: { error: reason },
- level: Sentry.Severity.Error,
- });
-
- // Swagger HTTP error
- if (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 something
- if (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 did
- Sentry.addBreadcrumb({
- category: 'Swagger',
- message: 'exists',
- data: { request },
- level: Sentry.Severity.Debug,
- });
-
- // Add breadcrumb with the error
- Sentry.addBreadcrumb({
- category: 'Swagger - Error',
- message: 'exists',
- data: { error: reason },
- level: Sentry.Severity.Error,
- });
-
- // Swagger HTTP error
- if (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 something
- if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {
- return;
- }
-
- // If the order already exists, just ignore the error
- if (reason.message.includes('orderId: This value is already used')) {
- return;
- }
-
- // Add breadcrumb with actual request we did
- Sentry.addBreadcrumb({
- category: 'Swagger',
- message: 'postQualityControlCollection',
- data: { request, element },
- level: Sentry.Severity.Debug,
- });
-
- // Add breadcrumb with the error
- Sentry.addBreadcrumb({
- category: 'Swagger - Error',
- message: 'postQualityControlCollection',
- data: { error: reason },
- level: Sentry.Severity.Error,
- });
-
- // Swagger HTTP error
- if (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 return
- if (this.setup) {
- return this;
- }
-
- // Get the username
- let 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 username
- username = 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 errors
- Sentry.setUser({ id: userHash, username });
-
- // Build all the clients
- this.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 up
- this.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 later
- const 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 loaded
- elementReady('.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 enabled
- if (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 supported
- if (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 object
- 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>`);
-
- // 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 album
- const $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 marker
- const 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 process
- Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);
-
- // Temp store deleteHash
- let deleteHash;
-
- try {
- // Create the album
- const response = await this.imgurClient.CreateAlbum(element);
- if (typeof response === 'undefined' || response == null) {
- return;
- }
-
- // Extract and build information needed
- deleteHash = response.data.deletehash;
- const albumId = response.data.id;
-
- // Upload all QC images
- const promises = [];
- $.each(element.imageUrls, (key, imageUrl) => {
- // Convert to base64, since Imgur cannot access our images
- promises.push(toDataURL(imageUrl).then(async (data) => {
- // Store our base64 and if the file is WEBP, convert it to JPG
- let base64Image = data;
- if (base64Image.indexOf('image/webp') !== -1) {
- base64Image = await WebpToJpg(base64Image);
- }
-
- // Remove the unnecessary `data:` part
- const cleanedData = base64Image.replace(/(data:image\/.*;base64,)/, '');
-
- // Upload the image to the album
- return this.imgurClient.AddBase64ImageToAlbum(cleanedData, deleteHash, element.purchaseUrl);
- }));
- });
-
- // Wait until everything has been tried to be uploaded
- await 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 background
- Snackbar('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 album
- const $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 marker
- const 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 album
- this.imgurClient.RemoveAlbum(deleteHash);
-
- // Reset the button
- $processing.remove();
- $base.show();
-
- // Show the error
- Snackbar(err.message, 'error');
-
- // If it's the slow down error, don't log it
- if (err instanceof ImgurSlowdownError) {
- return;
- }
-
- // Log the error
- Sentry.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 element
- await $.get(element.qcImagesUrl).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 writer
- const zipWriter = new zip.ZipWriter(new zip.Data64URIWriter('application/zip'));
-
- // Download all the images and add to the zip
- const 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 ZIP
- await Promise.all(promises);
-
- // Close the ZipWriter object and download to computer
- saveAs(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 element
- await $.get(element.qcImagesUrl).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 order
- return 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 images
- this.imageUrls = [];
-
- // Temporary items
- const 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 details
- this.orderId = this.element.parent().attr('data-id');
-
- // Item name
- this.title = truncate(removeWhitespaces(itemLink.text()), 255);
-
- // Purchase details
- this.website = determineWebsite(itemLink.attr('href'));
- this.purchaseUrl = cleanPurchaseUrl(itemLink.attr('href'), this.website);
-
- // Item price
- this.itemPrice = `CNY ${removeWhitespaces(parentTableEntry.find('td:nth-child(4) > span:nth-child(1)').text())}`;
-
- // Freight price
- this.freightPrice = `CNY ${removeWhitespaces(parentTableEntry.find('td:nth-child(5) span').text())}`;
-
- // Item weight
- const 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 ever
- this.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 client
- try {
- // Grab the OSS client
- const WindowOSS = await this._waitForValue('OSS');
-
- // Build the config for the bucket
- const 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 use
- Logger.info('OSS config build', config);
-
- // Set up the bucket for easy use
- this.window.client = new WindowOSS.Wrapper(config);
-
- // Mark as ready
- this.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 exists
- if (this.window[value]) {
- resolve(this.window[value]);
-
- return;
- }
-
- const _waitForGlobal = () => {
- if (this.window[value]) {
- resolve(this.window[value]);
-
- return;
- }
-
- // Wait until we have it
- setTimeout(() => { _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 return
- if (this.setup) {
- return this;
- }
-
- // Get the username
- const 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 error
- const userHash = SparkMD5.hash(username);
- Sentry.setUser({ id: userHash, username });
-
- // Build all the clients
- this.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 up
- this.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 later
- const 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 client
- this.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 with
- const result = await this.ossClient.list(element.orderId);
- if (typeof result.objects === 'undefined') {
- return;
- }
-
- // This plugin only works for certain websites, so check if element is supported
- if (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 buttons
- const $other = $this.parents('ul').first();
-
- // Show simple loading animation
- const $loading = $(`<ul class="badge-lists"><li style="cursor: wait"><img src="${Loading}" alt="Loading..." style="width: 100%"></li></ul>`);
- $other.after($loading);
-
- // Define upload object
- 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>`);
-
- // 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 album
- const $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 marker
- const 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 result = await this.ossClient.list(element.orderId);
- if (typeof result.objects === 'undefined') {
- Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);
- return;
- }
-
- result.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 process
- Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);
-
- // Temp store deleteHash
- let deleteHash;
-
- try {
- // Create the album
- const response = await this.imgurClient.CreateAlbum(element);
- if (typeof response === 'undefined' || response == null) {
- return;
- }
-
- // Extract and build information needed
- deleteHash = response.data.deletehash;
- const albumId = response.data.id;
-
- // Upload all QC images
- const promises = [];
- $.each(element.imageUrls, (key, imageUrl) => {
- promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl));
- });
-
- // Wait until everything has been tried to be uploaded
- await 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 background
- Snackbar('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 album
- const $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 marker
- const 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 album
- this.imgurClient.RemoveAlbum(deleteHash);
-
- // Reset the button
- $processing.remove();
- $base.show();
-
- // Show the error
- Snackbar(err.message, 'error');
-
- // If it's the slow down error, don't log it
- if (err instanceof ImgurSlowdownError) {
- return;
- }
-
- // Log the error
- Sentry.captureException(err);
- Logger.error(err);
- }
- }
- }
-
- class WeGoBuyElement {
- constructor($element) {
- this.element = $element;
-
- // Order details
- this.orderId = removeWhitespaces($element.find('td:nth-child(1) > p').text());
- this.imageUrls = $element.find('.lookPic').map((key, value) => $(value).attr('href')).get();
-
- // Item name
- this.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 price
- const 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 price
- 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]+)/);
- this.freightPrice = `${freightPriceMatches[1]} ${freightPriceMatches[2]}`;
-
- // Item weight
- this.weight = null;
-
- // Purchase details
- const 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 ever
- this.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 return
- if (this.setup) {
- return this;
- }
-
- // Ensure the toast looks decent on SB/WGB
- GM_addStyle('.swal2-popup.swal2-toast .swal2-title {font-size: 1em; font-weight: bolder}');
-
- // Get the username
- const 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 error
- const userHash = SparkMD5.hash(username);
- Sentry.setUser({ id: userHash, username });
-
- // Build all the clients
- this.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 setup
- this.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 later
- const 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 options
- if (element.imageUrls.length === 0) {
- return;
- }
-
- // Define column in which to download button
- const $inspection = $this.find('td:nth-child(6)').first();
-
- // Append download button if enabled
- if (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 supported
- if (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 animation
- const $loading = $(`<div style="padding:5px;"><span style="cursor: wait;"><img src="${Loading}" alt="Loading..."></span></div>`);
- $inspection.append($loading);
-
- // Define upload object
- const $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 album
- const $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 marker
- const 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 writer
- const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'));
-
- // Download all the images and add to the zip
- const 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 ZIP
- await Promise.all(promises);
-
- // Close the ZipWriter object and download to computer
- saveAs(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 process
- Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);
-
- // Temp store deleteHash
- let deleteHash;
-
- try {
- // Create the album
- const response = await this.imgurClient.CreateAlbum(element);
- if (typeof response === 'undefined' || response == null) {
- return;
- }
-
- // Extract and build information needed
- deleteHash = response.data.deletehash;
- const albumId = response.data.id;
-
- // Upload all QC images
- const promises = [];
- $.each(element.imageUrls, (key, imageUrl) => {
- promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl));
- });
-
- // Wait until everything has been tried to be uploaded
- await 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 background
- Snackbar('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 buttons
- const 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 album
- this.imgurClient.RemoveAlbum(deleteHash);
-
- // Reset the button
- $processing.remove();
- $base.show();
-
- // Show the error
- Snackbar(err.message, 'error');
-
- // If it's the slow down error, don't log it
- if (err instanceof ImgurSlowdownError) {
- return;
- }
-
- // Log the error
- Sentry.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 style
- GM_addStyle(GM_getResourceText('sweetalert2'));
-
- // Setup proper settings menu
- GM_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 changed
- GM_config.onclose = (saveFlag) => {
- if (saveFlag) {
- window.location.reload();
- }
- };
-
- // Register menu within GM
- GM_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://474c3febc82e44b8b283f23dacb76444@o740964.ingest.sentry.io/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 any
- const 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 page
- try {
- 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);
- }
- }());