// ==UserScript== // @name FR:Reborn - Agents extension // @namespace https://www.reddit.com/user/RobotOilInc // @version 1.0.2 // @description Upload Taobao and Yupoo QCs from your favorite agent to Imgur + QC server // @author RobotOilInc // @match https://www.basetao.net/index/myhome/myorder/* // @match https://basetao.net/index/myhome/myorder/* // @match https://www.basetao.com/index/myhome/myorder/* // @match https://basetao.com/index/myhome/myorder/* // @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.qc-server.cf/ // @supportURL https://greasyfork.org/en/scripts/426977-fr-reborn-agents-extension // @include https://www.basetao.net/index/orderphoto/itemimg/* // @include https://basetao.net/index/orderphoto/itemimg/* // @include https://www.basetao.com/index/orderphoto/itemimg/* // @include https://basetao.com/index/orderphoto/itemimg/* // @require https://unpkg.com/sweetalert2@11/dist/sweetalert2.min.js // @require https://unpkg.com/js-logger@1.6.1/src/logger.min.js // @require https://unpkg.com/spark-md5@3.0.1/spark-md5.min.js // @require https://unpkg.com/jquery@3.6.0/dist/jquery.min.js // @require https://unpkg.com/@phamthaibaoduy/jquery.ajax-retry@1.0.0/dist/jquery.ajax-retry.min.js // @require https://unpkg.com/@sentry/browser@6.5.1/build/bundle.min.js // @require https://unpkg.com/@sentry/tracing@6.5.1/build/bundle.tracing.min.js // @require https://unpkg.com/swagger-client@3.13.3/dist/swagger-client.browser.min.js // @require https://greasyfork.org/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-start // @icon https://i.imgur.com/mYBHjAg.png // @downloadURL none // ==/UserScript== 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} */ 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} */ 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')); }; }); }; // Possible sources const TAOBAO = 'taobao'; const YUPOO = 'yupoo'; const WEIDIAN = 'weidian'; const UNKNOWN = 'unknown'; const isUrl = (string) => { try { return Boolean(new URL(string)); } catch (e) { return false; } }; /** * @param url {string} * @returns {boolean} */ const supportedPurchaseWebsite = (url) => url.indexOf('item.taobao.com') !== -1 || url.indexOf('tmall.com') !== -1 || url.indexOf('yupoo.com') !== -1 || url.indexOf('weidian.com') !== -1; /** * @param originalUrl {string} * @param website {string} * @returns {string} */ const buildPurchaseUrl = (originalUrl, website) => { const idMatches = originalUrl.match(/[?&]id=(\d+)|[?&]itemID=(\d+)|\/albums\/(\d+)/i); const authorMatches = originalUrl.match(/https?:\/\/(\w+)\.x\.yupoo\.com/); if (website === TAOBAO) { return `https://item.taobao.com/item.htm?id=${idMatches[1]}`; } if (website === WEIDIAN) { return `https://weidian.com/item.html?itemID=${idMatches[2]}`; } if (website === YUPOO) { return `https://${authorMatches[1]}.x.yupoo.com/albums/${idMatches[3]}`; } throw new Error('could not build purchase URL'); }; /** * @param originalUrl {string} * @returns {string} */ const determineWebsite = (originalUrl) => { if (originalUrl.indexOf('item.taobao.com') !== -1 || originalUrl.indexOf('tmall.com') !== -1) { return TAOBAO; } if (originalUrl.indexOf('yupoo.com') !== -1) { return YUPOO; } if (originalUrl.indexOf('weidian.com') !== -1) { return WEIDIAN; } return UNKNOWN; }; class BaseTaoElement { constructor($element) { const $baseElement = $element.parents('tr').find('td[colspan=\'2\']').first(); this.object = $element; this.imageUrls = []; this.qcImagesUrl = $element.parent().attr('href').trim(); this.url = $baseElement.find('.goodsname_color').first().attr('href').trim(); this.website = determineWebsite(this.url); this.title = truncate(removeWhitespaces($baseElement.find('.goodsname_color').first().text()), 255); this.size = truncate(removeWhitespaces($baseElement.find('.size_color_color:nth-child(2) > u').text()), 255); const color = removeWhitespaces($baseElement.find('.size_color_color:nth-child(1) > u').text()); if (color !== '-' && color !== 'NO') { this.color = truncate(color, 255); } this.itemPrice = `CNY ${removeWhitespaces($element.parents('tr').find('td:nth-child(2) > span').first().text())}`; this.freightPrice = `CNY ${removeWhitespaces($element.parents('tr').find('td:nth-child(3) > span').first().text())}`; // eslint-disable-next-line prefer-destructuring this.orderId = this.qcImagesUrl.match(/itemimg\/(\d+)\.html/)[1]; } /** * @returns {{color, size}} */ get sizingInfo() { return { color: this.color, size: this.size }; } /** * @param imageUrls {string[]} */ set images(imageUrls) { this.imageUrls = imageUrls; } /** * @returns {string} */ get purchaseUrl() { return buildPurchaseUrl(this.url, this.website); } } /** * @param text {string} * @param type {null|('success'|'error'|'warning'|'info')} */ const Snackbar = function (text, type = null) { if (typeof type !== 'undefined' && type != null) { Swal.fire({ title: text, icon: type, position: 'bottom-end', showConfirmButton: false, allowOutsideClick: false, backdrop: false, timer: 1500, }); return; } Swal.fire({ title: text, position: 'bottom-end', showConfirmButton: false, allowOutsideClick: false, backdrop: false, timer: 1500, }); }; 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')}` }; 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>} */ CreateAlbum(options) { return $.ajax({ url: `https://${this.host}/3/album`, type: 'POST', headers: this.headers, data: { title: options.title, privacy: 'hidden', description: `Auto uploaded using FR:Reborn - ${this.agent} ${this.version}`, }, }).retry({ times: 3 }).catch((err) => { // Log the error somewhere Logger.error(`Could not make an album: ${err.statusText}`, err); // If we uploaded too fast, tell the user if (err.responseJSON.data.error.code === 429) { Snackbar(`Imgur is telling us to slow down: ${err.responseJSON.data.error.message}`, 'error'); return; } // Tell the user that "something" is wrong Snackbar('Could not make an album, please try again later...', 'error'); Sentry.captureException(err); }); } /** * @param base64Image {string} * @param deleteHash {string} * @param purchaseUrl {string} * @returns {Promise<*|null>} */ async AddBase64ImageToAlbum(base64Image, deleteHash, purchaseUrl) { return $.ajax({ url: `https://${this.host}/3/image`, headers: this.headers, type: 'POST', data: { album: deleteHash, type: 'base64', image: base64Image, description: `W2C: ${purchaseUrl}`, }, }).retry({ times: 3 }).then(() => true).catch((err) => { // If we uploaded too many files, tell the user if (err.responseJSON.data.error.code === 429) { return false; } // Log the error otherwise Logger.error('An error happened when uploading the image', err); Sentry.captureException(err); return false; }); } /** * @param imageUrl {string} * @param deleteHash {string} * @param purchaseUrl {string} * @returns {Promise<*|null>} */ async AddImageToAlbum(imageUrl, deleteHash, purchaseUrl) { return $.ajax({ url: `https://${this.host}/3/image`, headers: this.headers, type: 'POST', data: { album: deleteHash, image: imageUrl, description: `W2C: ${purchaseUrl}`, }, }).retry({ times: 3 }).then(() => true).catch((err) => { // If we uploaded too many files, tell the user if (err.responseJSON.data.error.code === 429) { return false; } // Log the error otherwise Logger.error('An error happened when uploading the image', err); Sentry.captureException(err); return false; }); } /** * @param deleteHash {string} */ RemoveAlbum(deleteHash) { $.ajax({ url: `https://${this.host}/3/album/${deleteHash}`, headers: this.headers, type: 'DELETE' }).retry({ times: 3 }); } } 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} */ constructor(version, client, userHash) { this.version = version; this.client = client; this.userHash = userHash; } /** * @param element * @returns {Promise} */ existingAlbumByOrderId(element) { return this.client.apis.QualityControl.hasUploaded({ usernameHash: this.userHash, orderId: element.orderId, }).then((response) => { if (typeof response.body === 'undefined') { return null; } if (!response.body.success) { return null; } return response.body.albumId; }).catch((reason) => { // Swagger HTTP error if (typeof reason.response !== 'undefined') { Sentry.addBreadcrumb({ category: 'Swagger', message: JSON.stringify(reason), level: Sentry.Severity.Debug }); Sentry.captureException(buildSwaggerHTTPError(reason.response)); Logger.error('Could not check if the album exists on the QC server'); return '-1'; } Sentry.addBreadcrumb({ category: 'Swagger', message: JSON.stringify(reason), level: Sentry.Severity.Debug }); Sentry.captureException(new Error('Could not check if the album exists on the QC server')); Logger.error('Could not check if the album exists on the QC server', reason); return '-1'; }); } /** * @param url {string} * @returns {Promise} */ exists(url) { return this.client.apis.QualityControl.exists({ url }).then((response) => { if (typeof response.body === 'undefined') { return null; } if (!response.body.success) { return null; } return response.body.exists; }).catch((reason) => { // Swagger HTTP error if (typeof reason.response !== 'undefined') { Sentry.addBreadcrumb({ category: 'Swagger', message: JSON.stringify(reason), level: Sentry.Severity.Debug }); Sentry.captureException(buildSwaggerHTTPError(reason.response)); Logger.error('Could not check if the album exists on the QC server'); return false; } Sentry.addBreadcrumb({ category: 'Swagger', message: JSON.stringify(reason), level: Sentry.Severity.Debug }); Sentry.captureException(new Error('Could not check if the album exists on the QC server')); Logger.error('Could not check if the album exists on the QC server', reason); return false; }); } /** * @param element {BaseTaoElement|WeGoBuyElement} * @param album {string} */ uploadQc(element, album) { return this.client.apis.QualityControl.postQualityControlCollection({}, { method: 'post', requestContentType: 'application/json', requestBody: { usernameHash: this.userHash, albumId: album, color: element.color, orderId: element.orderId, purchaseUrl: element.purchaseUrl, sizing: element.size, itemPrice: element.itemPrice, freightPrice: element.freightPrice, source: `BaseTao to Imgur ${this.version}`, website: element.website, }, }).catch((reason) => { // Swagger HTTP error if (typeof reason.response !== 'undefined') { Sentry.addBreadcrumb({ category: 'Swagger', message: JSON.stringify(reason), level: Sentry.Severity.Debug }); Sentry.captureException(buildSwaggerHTTPError(reason.response)); Logger.error('Could not check if the album exists on the QC server'); return; } Sentry.addBreadcrumb({ category: 'Swagger', message: JSON.stringify(reason), level: Sentry.Severity.Debug }); Sentry.captureException(new Error('Could not check if the album exists on the QC server')); Logger.error('Could not check if the album exists on the QC server', reason); }); } } class BaseTao { constructor() { this.setup = false; } /** * @param hostname {string} * @returns {boolean} */ supports(hostname) { return hostname.includes('basetao.com'); } /** * @param client {SwaggerClient} * @returns {Promise} */ async build(client) { // If already build before, just return if (this.setup) { return this; } this.client = client; this.imgurClient = new Imgur(GM_info.script.version, GM_config, 'BaseTao'); // Get the username const username = $('#dropdownMenu1').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, but use the username hash const userHash = SparkMD5.hash(username); Sentry.setUser({ id: userHash }); // Create QC client with user hash this.qcClient = new QC(GM_info.script.version, this.client, userHash); // Mark that this agent has been setup this.setup = true; return this; } /** * @param element {BaseTaoElement} * @returns {Promise} */ async uploadToImgur(element) { if (this.setup === false) { throw new Error('Agent is not setup, so cannot be used'); } const $processing = $('
  • Processing...
'); const $base = element.object.parents('td').first().find('ul:last-child').first(); $base.after($processing).hide(); Snackbar('Pictures are being uploaded....'); // Create the album const response = await this.imgurClient.CreateAlbum(element); if (typeof response === 'undefined' || response == null) { return; } const deleteHash = response.data.deletehash; const albumId = response.data.id; const AlbumLink = `https://imgur.com/a/${albumId}`; // Upload all QC images let uploadedImages = 0; 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); }).then((uploaded) => { if (uploaded === false) { return; } uploadedImages++; })); }); // Wait until everything has been tried to be uploaded await Promise.all(promises); // If not all images have been uploaded, abort everything if (uploadedImages !== element.imageUrls.length) { Snackbar('Imgur is rate-limiting you, try again later.', 'error'); this.imgurClient.RemoveAlbum(deleteHash); $processing.remove(); $base.show(); return; } // Tell the user it was uploaded and open the album in the background Snackbar('Pictures have been uploaded!', 'success'); GM_openInTab(AlbumLink, true); // Tell QC Suite about our uploaded QC's (if it's from TaoBao) if (supportedPurchaseWebsite(element.url)) { this.qcClient.uploadQc(element, albumId); } // Wrap the logo in a href to the new album const $image = $base.find('img'); $image.wrap(``); $image.removeAttr('title'); // Remove processing $processing.remove(); // Update the marker const $qcMarker = $base.find('.qc-marker:not(:first-child)').first(); $qcMarker.attr('title', 'You have uploaded your QC') .css('cursor', 'help') .css('color', 'green') .text('✓'); // Remove the click handler $base.off(); // Show it again $base.show(); } async uploadHandler(element) { if (this.setup === false) { throw new Error('Agent is not setup, so cannot be used'); } // Go to the QC pictures URL and grab all image src's $.get(element.qcImagesUrl, (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; } // Add all image urls to the element $('
').html(data).find('div.container.container-top60 > img').each(function () { element.imageUrls.push($(this).attr('src')); }); // Finally go and upload the order this.uploadToImgur(element); }); } 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; $('.myparcels-ul').first().find('span.glyphicon.glyphicon-picture').each(async function () { const $this = $(this); const element = new BaseTaoElement($this); // This plugin only works for certain websites, so check if element is supported if (supportedPurchaseWebsite(element.url) === false) { const $upload = $('
  • Create a basic album
'); $upload.find('span').first().after($('')); $upload.on('click', () => { agent.uploadHandler(element); }); $this.parents('td').first().append($upload); return; } const $loading = $('
  • Loading...
'); $this.parents('td').first().append($loading); // Define upload object const $upload = $('
  • Upload your QC
'); // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader. const albumId = await agent.qcClient.existingAlbumByOrderId(element); if (albumId === '-1') { $upload.find('span').first().html($('⚠️')); $this.parents('td').first().append($upload); $loading.remove(); return; } // Has anyone ever uploaded a QC, if not, show a red marker const exists = await agent.qcClient.exists(element.purchaseUrl); if (!exists) { $upload.find('span').first().after($('(!)')); $upload.on('click', () => { agent.uploadHandler(element); }); $this.parents('td').first().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($('')); $image.wrap(``); $image.removeAttr('title'); $this.parents('td').first().append($upload); $loading.remove(); return; } // A previous QC exists, but you haven't uploaded yours yet, show orange marker $upload.find('span').first().after($('(!)')); $upload.on('click', () => { agent.uploadHandler(element); }); $this.parents('td').first().append($upload); $loading.remove(); }); } } class WeGoBuyElement { constructor($element) { this.object = $element; // Ordere details this.orderId = removeWhitespaces($element.find('table > tbody > tr:nth-child(1) > td:nth-child(1) > p').text()); this.imageUrls = $element.find('.lookPic').map((key, value) => $(value).attr('href')).get(); // Item details this.title = truncate(removeWhitespaces($element.find('.js-item-title').first().text()), 255); this.size = truncate(removeWhitespaces($element.find('.user_orderlist_txt').text()), 255); // Price details const itemPriceMatches = truncate(removeWhitespaces($element.find('tbody > tr > td:nth-child(2)').first().text()), 255) .match(/([A-Z]{2})\W+([.,0-9]+)/); this.itemPrice = `${itemPriceMatches[1]} ${itemPriceMatches[2]}`; const freightPriceMatches = truncate(removeWhitespaces($element.find('tbody > tr:nth-child(1) > td:nth-child(8) > p:nth-child(2)').first().text()), 255) .match(/([A-Z]{2})\W+([.,0-9]+)/); this.freightPrice = `${freightPriceMatches[1]} ${freightPriceMatches[2]}`; // Purchase details const possibleUrl = removeWhitespaces($element.find('.js-item-title').first().attr('href')).trim(); this.url = isUrl(possibleUrl) ? possibleUrl : ''; if (this.url.length !== 0) { this.website = determineWebsite(this.url); } } /** * @returns {{color, size}} */ get sizingInfo() { return { size: this.size }; } /** * @param imageUrls {string[]} */ set images(imageUrls) { this.imageUrls = imageUrls; } /** * @returns {string} */ get purchaseUrl() { return buildPurchaseUrl(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'); } /** * @param client {SwaggerClient} * @returns {Promise} */ async build(client) { // If already build before, just return if (this.setup) { return this; } this.client = client; this.imgurClient = new Imgur(GM_info.script.version, GM_config, 'WeGoBuy'); // 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, but use the username hash const userHash = SparkMD5.hash(username); Sentry.setUser({ id: userHash }); // Create QC client this.qcClient = new QC(GM_info.script.version, this.client, userHash); // Mark that this agent has been setup this.setup = true; return this; } /** * @param element {WeGoBuyElement} * @returns {Promise} */ async UploadToImgur(element) { if (this.setup === false) { throw new Error('Agent is not setup, so cannot be used'); } const $processing = $('

FR:Reborn:

Processing...
'); const $options = element.object.find('tbody > tr > td:nth-child(7)').first(); const $base = $options.find('div').first(); $base.after($processing).hide(); Snackbar('Pictures are being uploaded....'); // Create the album const response = await this.imgurClient.CreateAlbum(element); if (typeof response === 'undefined' || response == null) { return; } const deleteHash = response.data.deletehash; const albumId = response.data.id; const AlbumLink = `https://imgur.com/a/${albumId}`; // Upload all QC images let uploadedImages = 0; const promises = []; $.each(element.imageUrls, (key, imageUrl) => { promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl).then((uploaded) => { if (uploaded === false) { return; } uploadedImages++; })); }); // Wait until everything has been tried to be uploaded await Promise.all(promises); // If not all images have been uploaded, abort everything if (uploadedImages !== element.imageUrls.length) { Snackbar('Imgur is rate-limiting you, try again later.', 'error'); this.imgurClient.RemoveAlbum(deleteHash); $processing.remove(); $base.show(); return; } // Tell the user it was uploaded and open the album in the background Snackbar('Pictures have been uploaded!', 'success'); GM_openInTab(AlbumLink, true); // Tell QC Suite about our uploaded QC's (if it's from TaoBao) if (supportedPurchaseWebsite(element.url)) { this.qcClient.uploadQc(element, albumId); } // Remove processing $processing.remove(); $base.remove(); // Add new buttons $options.append($('

FR:Reborn:

' + `Go to album` + '' + '
')); // Remove the click handler $base.off(); // Show it again $base.show(); } 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; $('#table_list > div').each(async function () { const $this = $(this); const element = new WeGoBuyElement($this); // No pictures (like rehearsal orders), or no URL like service fees, no QC options if (element.imageUrls.length === 0 || element.url.length === 0) { return; } // This plugin only works for certain websites, so check if element is supported if (supportedPurchaseWebsite(element.url) === false) { const $upload = $('

FR:Reborn:

Create a basic album
'); $upload.find('span').first().after($('')); $upload.on('click', () => { agent.UploadToImgur(element); }); $this.parents('td').first().append($upload); return; } // Define column in which to show buttons const $other = $this.find('tbody > tr > td:nth-child(7)').first(); // Show simple loading animation const $loading = $('

FR:Reborn:

Loading...
'); $other.append($loading); // Define upload object const $upload = $('

FR:Reborn:

Upload your QC
'); // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader. const albumId = await agent.qcClient.existingAlbumByOrderId(element); if (albumId === '-1') { $upload.find('span').first().after($('⚠️')); $upload.on('click', () => { agent.UploadToImgur(element); }); $other.append($upload); $loading.remove(); return; } // Has anyone ever uploaded a QC, if not, show a red marker const exists = await agent.qcClient.exists(element.purchaseUrl); if (!exists) { $upload.find('span').first().after($('(!)')); $upload.on('click', () => { agent.UploadToImgur(element); }); $other.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($('')); $image.wrap(``); $image.removeAttr('title'); $other.append($upload); $loading.remove(); return; } // A previous QC exists, but you haven't uploaded yours yet, show orange marker $upload.find('span').first().after($('(!)')); $upload.on('click', () => { agent.UploadToImgur(element); }); $other.append($upload); $loading.remove(); }); } } /** * @param hostname {string} */ function getAgent(hostname) { const agents = [new BaseTao(), new WeGoBuy()]; let agent = null; Object.values(agents).forEach((value) => { if (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.qc-server.cf/api/doc.json', }, 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', transport: Sentry.Transports.XHRTransport, release: `agents-${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, version ${GM_info.script.version}`); /** @type {SwaggerClient} */ let client; // Try to create Swagger client from our own documentation try { client = await new SwaggerClient({ url: GM_config.get('swaggerDocUrl') }); } catch (error) { 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; } // Get the proper source view, if any const agent = getAgent(window.location.hostname); if (agent !== null) { // Build proper agent and process page (await agent.build(client)).process(); return; } Sentry.captureMessage(`Unsupported website ${window.location.hostname}`); Logger.error('Unsupported website'); }());