// ==UserScript== // @name Stores to Agent // @namespace https://www.reddit.com/user/RobotOilInc // @version 3.4.2 // @description Adds an order directly from stores to your agent // @author RobotOilInc // @match https://detail.1688.com/offer/* // @match https://*.taobao.com/item.htm* // @match https://*.v.weidian.com/?userid=* // @match https://*.weidian.com/item.html* // @match https://*.yupoo.com/albums/* // @match https://detail.tmall.com/item.htm* // @match https://weidian.com/*itemID=* // @match https://weidian.com/?userid=* // @match https://weidian.com/item.html* // @match https://*.pandabuy.com/* // @match https://www.pandabuy.com/* // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_webRequest // @grant GM_xmlhttpRequest // @license MIT // @homepageURL https://greasyfork.org/en/scripts/427774-stores-to-agent // @supportURL https://greasyfork.org/en/scripts/427774-stores-to-agent // @require https://unpkg.com/js-logger@1.6.1/src/logger.min.js#sha256-05beef5b6fa26a30b9ec60404d31da048e67ae98da3657b4b6fa83274e428d37 // @require https://unpkg.com/jquery@3.6.0/dist/jquery.min.js#sha256-ff1523fb7389539c84c65aba19260648793bb4f5e29329d2ee8804bc37a3fe6e // @require https://greasyfork.org/scripts/401399-gm-xhr/code/GM%20XHR.js?version=938754#sha256-12de0fb77963c67f3612374ed9c53249f84ee6b1de2615471bf1158cb8052201 // @require https://greasyfork.org/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657#sha256-229668ef83cd26ac207e9d780e2bba6658e1506ac0b23fb29dc94ae531dd31fb // @connect basetao.com // @connect cssbuy.com // @connect superbuy.com // @connect ytaopal.com // @connect wegobuy.com // @connect pandabuy.com // @webRequest [{ "selector": "*thor.weidian.com/stardust/*", "action": "cancel" }] // @run-at document-end // @icon https://i.imgur.com/2lQXuqv.png // @downloadURL none // ==/UserScript== class PandaBuyError extends Error { constructor(message) { super(message); this.name = 'PandaBuyError'; } } const PandaBuyToken = 'PANDABUY_TOKEN'; const PandaBuyUserInfo = 'PANDABUY_USERINFO'; class PandaBuyLogin { /** * @param hostname {string} * @returns {boolean} */ supports(hostname) { return hostname.includes('pandabuy.com'); } process(window) { this._storeToken(window.localStorage); } /** * @param localStorage {Storage} * @private */ _storeToken(localStorage) { // If we already have a token, don't bother const currentToken = GM_getValue(PandaBuyToken, null); if (currentToken !== null && currentToken.length !== 0) { return; } // Don't bother with getting the token, if we aren't loggeed in yet const userInfo = localStorage.getItem(PandaBuyUserInfo); if (userInfo === null || userInfo.length === 0) { return; } // The token should now exist const updatedToken = localStorage.getItem(PandaBuyToken); if (updatedToken === null || updatedToken.length === 0) { throw new PandaBuyError('Could not retrieve token'); } // Store it internally GM_setValue(PandaBuyToken, updatedToken); Logger.info('Updated the PandaBuy Authorization Token'); } } /** * @param hostname {string} */ function getLogin(hostname) { const agents = [new PandaBuyLogin()]; let agent = null; Object.values(agents).forEach((value) => { if (value.supports(hostname)) { agent = value; } }); return agent; } class Item { /** * @param id {string|null} * @param name {string|null} * @param imageUrl {string|null} * @param model {string|null} * @param color {string|null} * @param size {string|null} * @param others {Array} */ constructor(id, name, imageUrl, model, color, size, others) { this._id = id; this._name = name; this._imageUrl = imageUrl; this._model = model; this._color = color; this._size = size; this._others = others; } get id() { return this._id; } get name() { return this._name; } get imageUrl() { return this._imageUrl; } get model() { return this._model; } get color() { return this._color; } get size() { return this._size; } /** * @return {string} */ get other() { return this._others.join('\n'); } } class Order { /** * @param shop {Shop} * @param item {Item} * @param price {Number} * @param shipping {Number} */ constructor(shop, item, price, shipping) { this._shop = shop; this._item = item; this._price = price; this._shipping = shipping; } get shop() { return this._shop; } get item() { return this._item; } get price() { return this._price; } get shipping() { return this._shipping; } } class Shop { /** * @param id {null|string} * @param name {null|string} * @param url {null|string} */ constructor(id, name, url) { this._shopId = id; this._shopName = name; this._shopUrl = url; } /** * @returns {null|string} */ get id() { return this._shopId; } /** * @returns {null|string} */ get name() { return this._shopName; } /** * @returns {null|string} */ get url() { return this._shopUrl; } } /** * @param toast {string} */ const Snackbar = function (toast) { // Log the snackbar, for ease of debugging Logger.info(toast); // Setup toast element const $toast = $(`
${toast}
`); // Append to the body $('body').append($toast); // Set a timeout to remove the toast setTimeout(() => $toast.fadeOut('slow', () => $toast.remove()), 5000); }; /** * Waits for an element satisfying selector to exist, then resolves promise with the element. * Useful for resolving race conditions. * * @param selector {string} * @returns {Promise} */ 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 BaseTaoError extends Error { constructor(message) { super(message); this.name = 'BaseTaoError'; } } /** * Removes all emojis from the input text. * * @param string {string} */ const removeEmoji = (string) => string.replace(/[^\p{L}\p{N}\p{P}\p{Z}^$\n]/gu, ''); /** * Trims the input text and removes all in between spaces as well. * * @param string {string} */ const removeWhitespaces = (string) => string.trim().replace(/\s(?=\s)/g, ''); const CSRF_REQUIRED_ERROR = 'You need to be logged in on BaseTao to use this extension (CSRF required).'; class BaseTao { get name() { return 'BaseTao'; } /** * @param order {Order} */ async send(order) { // Get proper domain to use const properDomain = await this._getDomain(); // Build the purchase data const purchaseData = await this._buildPurchaseData(properDomain, order); Logger.info('Sending order to BaseTao...', properDomain, purchaseData); // Do the actual call await $.ajax({ url: `${properDomain}/index/Ajax_data/buyonecart`, data: purchaseData, type: 'POST', headers: { origin: `${properDomain}`, referer: `${properDomain}/index/selfhelporder.html`, 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36', 'x-requested-with': 'XMLHttpRequest', }, }).then((response) => { if (removeWhitespaces(response) === '1') { return; } Logger.error('Item could not be added', response); throw new BaseTaoError('Item could not be added, make sure you are logged in'); }).catch((err) => { // If the error is our own, just rethrow it if (err instanceof BaseTaoError) { throw err; } Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } /** * @private * @returns {Promise} */ async _getDomain() { // Try HTTPS (with WWW) first let $data = $(await $.get('https://www.basetao.com/index/selfhelporder.html')); let csrfToken = $data.find('input[name=csrf_test_name]').first(); if (csrfToken.length !== 0 && csrfToken.val().length !== 0) { return 'https://www.basetao.com'; } // Try HTTPS (without WWW) after $data = $(await $.get('https://basetao.com/index/selfhelporder.html')); csrfToken = $data.find('input[name=csrf_test_name]').first(); if (csrfToken.length !== 0 && csrfToken.val().length !== 0) { return 'https://basetao.com'; } // User is not logged in/there is an issue throw new Error(CSRF_REQUIRED_ERROR); } /** * @private * @param properDomain {string} * @param order {Order} */ async _buildPurchaseData(properDomain, order) { // Get the CSRF token const csrf = await this._getCSRF(properDomain); // Build the data we will send return { csrf_test_name: csrf, color: order.item.color, size: order.item.size, number: 1, pric: order.price, shipping: order.shipping, totalpric: order.price + order.shipping, t_title: encodeURIComponent(removeEmoji(order.item.name)), t_seller: encodeURIComponent(removeEmoji(order.shop.name)), t_img: encodeURIComponent(order.item.imageUrl), t_href: encodeURIComponent(window.location.href), s_url: encodeURIComponent(order.shop.url), buyyourself: 1, note: this._buildRemark(order), item_id: order.item.id, sku_id: null, site: null, }; } /** * @private * @param properDomain {string} * @returns {Promise} */ async _getCSRF(properDomain) { // Grab data from BaseTao const data = await $.get(`${properDomain}/index/selfhelporder.html`); // Check if user is actually logged in if (data.indexOf('long time no operation ,please sign in again') !== -1) { throw new Error(CSRF_REQUIRED_ERROR); } // Convert into jQuery object const $data = $(data); // Get the username const username = $data.find('#dropdownMenu1').text(); if (typeof username === 'undefined' || username == null || username === '') { throw new Error(CSRF_REQUIRED_ERROR); } // Return CSRF return $data.find('input[name=csrf_test_name]').first().val(); } /** * @private * @param order {Order} * @returns {string|null} */ _buildRemark(order) { const descriptionParts = []; if (order.item.model !== null) descriptionParts.push(`Model: ${order.item.model}`); if (order.item.other.length !== 0) descriptionParts.push(order.item.other); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } } class CSSBuyError extends Error { constructor(message) { super(message); this.name = 'CSSBuyError'; } } class CSSBuy { get name() { return 'CSSBuy'; } /** * @param order {Order} */ async send(order) { // Build the purchase data const purchaseData = this._buildPurchaseData(order); Logger.info('Sending order to CSSBuy...', purchaseData); // Do the actual call await $.ajax({ url: 'https://www.cssbuy.com/ajax/fast_ajax.php?action=buyone', data: purchaseData, dataType: 'json', type: 'POST', headers: { origin: 'https://www.cssbuy.com/item.html', referer: 'https://www.cssbuy.com/item.html', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36', }, }).then((response) => { if (response.ret === 0) { return; } Logger.error('Item could not be added', response); throw new CSSBuyError('Item could not be added'); }).catch((err) => { // If the error is our own, just rethrow it if (err instanceof CSSBuyError) { throw err; } Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } /** * @private * @param order {Order} * @return {object} */ _buildPurchaseData(order) { // Build the description const description = this._buildRemark(order); // Create the purchasing data return { data: { buynum: 1, shopid: order.shop.id, picture: order.item.imageUrl, defaultimg: order.item.imageUrl, freight: order.shipping, price: order.price, color: order.item.color, colorProp: null, size: order.item.size, sizeProp: null, usd_price: null, usd_freight: null, usd_total_price: null, total: order.price + order.shipping, buyyourself: 0, seller: order.shop.name, href: window.location.href, title: order.item.name, note: description, expressno: null, promotionCode: null, option: description, }, }; } /** * @private * @param order {Order} * @returns {string|null} */ _buildRemark(order) { const descriptionParts = []; if (order.item.model !== null) descriptionParts.push(`Model: ${order.item.model}`); if (order.item.color !== null) descriptionParts.push(`Color: ${order.item.color}`); if (order.item.size !== null) descriptionParts.push(`Size: ${order.item.size}`); if (order.item.other.length !== 0) descriptionParts.push(`${order.item.other}`); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } } class PandaBuy { get name() { return 'PandaBuy'; } /** * @param order {Order} */ async send(order) { const token = GM_getValue(PandaBuyToken, null); if (token === null || token.length === 0) { throw new PandaBuyError('We do not have your PandaBuy authorization token yet. Please visit PandaBuy (and login if needed)'); } // Build the purchase data const purchaseData = this._buildPurchaseData(order); Logger.info('Sending order to PandaBuy...', purchaseData); // Do the actual call await $.ajax({ url: 'https://www.pandabuy.com/gateway/user/cart/add', headers: { accept: 'application/json, text/plain, */*', authorization: `Bearer ${token}`, 'content-type': 'application/json;charset=UTF-8', currency: 'CNY', lang: 'en', }, referrer: `https://www.pandabuy.com/uniorder?text=${encodeURIComponent(window.location.href)}`, referrerPolicy: 'strict-origin-when-cross-origin', data: JSON.stringify(purchaseData), dataType: 'json', type: 'POST', mode: 'cors', credentials: 'include', }).then((response) => { if (response.code === 200 && response.msg === null) { return; } Logger.error('Item could not be added', response.msg); throw new PandaBuyError('Item could not be added'); }).catch((err) => { // If the error is our own, just rethrow it if (err instanceof PandaBuyError) { throw err; } // Our token has expired if (err.status === 401) { // Reset the current token GM_setValue(PandaBuyToken, null); Logger.error('PandaBuy authorization token has expired'); throw new PandaBuyError('Your PandaBuy authorization token has expired. Please visit PandaBuy (or login at PandaBuy) again'); } Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } /** * @private * @param order {Order} * @return {object} */ _buildPurchaseData(order) { // Build the description const description = this._buildRemark(order); // Create the purchasing data return { storeName: order.shop.name, storeId: order.shop.id, goodsUrl: window.location.href, goodsName: order.item.name, goodsAttr: description, storageNo: 1, goodsPrice: order.price, goodsNum: 1, fare: order.shipping, remark: '|', selected: 1, purchaseType: 1, goodsImg: order.item.imageUrl, servicePrice: '0.00', writePrice: order.price, actPrice: order.price, storeSource: this._buildStoreSource(), goodsId: order.item.id, }; } /** * @private * @param order {Order} * @returns {string|null} */ _buildRemark(order) { const descriptionParts = []; if (order.item.model !== null && order.item.model.length !== 0) descriptionParts.push(`Model: ${order.item.model}`); if (order.item.color !== null && order.item.color.length !== 0) descriptionParts.push(`Color: ${order.item.color}`); if (order.item.size !== null && order.item.size.length !== 0) descriptionParts.push(`Size: ${order.item.size}`); if (order.item.other.length !== 0) descriptionParts.push(`${order.item.other}`); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } /** * @private * @returns {string} */ _buildStoreSource() { if (window.location.hostname.includes('1688.com')) { return '1688'; } if (window.location.hostname.includes('taobao.com')) { return 'taobao'; } if (window.location.hostname.includes('weidian.com')) { return 'weidian'; } if (window.location.hostname.includes('yupoo.com')) { return 'yupoo'; } if (window.location.hostname === 'detail.tmall.com') { return 'tmall'; } throw new PandaBuyError(`Could not determine store source ${window.location.hostname}`); } } class BuildTaoCarts { /** * @param order {Order} */ purchaseData(order) { // Build the description const description = this._buildRemark(order); // Generate an SKU based on the description // eslint-disable-next-line no-bitwise const sku = description.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); // Create the purchasing data return { type: 1, shopItems: [{ shopLink: '', shopSource: 'NOCRAWLER', shopNick: '', shopId: '', goodsItems: [{ beginCount: 0, count: 1, desc: description, freight: order.shipping, freightServiceCharge: 0, goodsAddTime: Math.floor(Date.now() / 1000), goodsCode: `NOCRAWLER-${sku}`, goodsId: window.location.href, goodsLink: window.location.href, goodsName: order.item.name, goodsPrifex: 'NOCRAWLER', goodsRemark: description, guideGoodsId: '', is1111Yushou: 'no', picture: order.item.imageUrl, platForm: 'pc', price: order.price, priceNote: '', serviceCharge: 0, sku: order.item.imageUrl, spm: '', warehouseId: '1', }], }], }; } /** * @param order {Order} * @returns {string|null} */ _buildRemark(order) { const descriptionParts = []; if (order.item.model !== null) descriptionParts.push(`Model: ${order.item.model}`); if (order.item.color !== null) descriptionParts.push(`Color: ${order.item.color}`); if (order.item.size !== null) descriptionParts.push(`Size: ${order.item.size}`); if (order.item.other.length !== 0) descriptionParts.push(`${order.item.other}`); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } } class SuperBuyError extends Error { constructor(message) { super(message); this.name = 'SuperBuyError'; } } class SuperBuy { constructor() { this._builder = new BuildTaoCarts(); } get name() { return 'SuperBuy'; } /** * @param order {Order} */ async send(order) { // Build the purchase data const purchaseData = this._builder.purchaseData(order); Logger.info('Sending order to SuperBuy...', purchaseData); // Do the actual call await $.ajax({ url: 'https://front.superbuy.com/cart/add-cart', data: JSON.stringify(purchaseData), dataType: 'json', type: 'POST', headers: { origin: 'https://www.superbuy.com', referer: 'https://www.superbuy.com/', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36', }, }).then((response) => { if (response.state === 0 && response.msg === 'Success') { return; } Logger.error('Item could not be added', response.msg); throw new SuperBuyError('Item could not be added'); }).catch((err) => { // If the error is our own, just rethrow it if (err instanceof SuperBuyError) { throw err; } Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } } class WeGoBuyError extends Error { constructor(message) { super(message); this.name = 'WeGoBuyError'; } } class WeGoBuy { constructor() { this._builder = new BuildTaoCarts(); } get name() { return 'WeGoBuy'; } /** * @param order {Order} */ async send(order) { // Build the purchase data const purchaseData = this._builder.purchaseData(order); Logger.info('Sending order to WeGoBuy...', purchaseData); // Do the actual call await $.ajax({ url: 'https://front.wegobuy.com/cart/add-cart', data: JSON.stringify(purchaseData), dataType: 'json', type: 'POST', headers: { origin: 'https://www.wegobuy.com', referer: 'https://www.wegobuy.com/', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36', }, }).then((response) => { if (response.state === 0 && response.msg === 'Success') { return; } Logger.error('Item could not be added', response.msg); throw new WeGoBuyError('Item could not be added'); }).catch((err) => { // If the error is our own, just rethrow it if (err instanceof WeGoBuyError) { throw err; } Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } } class YtaopalError extends Error { constructor(message) { super(message); this.name = 'YtaopalError'; } } class Ytaopal { get name() { return 'Ytaopal'; } /** * @param order {Order} */ async send(order) { // Build the purchase data const purchaseData = this._buildPurchaseData(order); Logger.info('Sending order to Ytaopal...', purchaseData); // Do the actual call await $.ajax({ url: 'https://www.ytaopal.com/Cart/Add', data: purchaseData, dataType: 'json', type: 'POST', headers: { origin: 'https://www.ytaopal.com/Cart/Add', referer: 'https://www.ytaopal.com/Cart/Add', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36', }, }).then((response) => { if (response.status !== 0) { return; } Logger.error('Item could not be added', response); throw new YtaopalError(response.info); }).catch((err) => { // If the error is our own, just rethrow it if (err instanceof YtaopalError) { throw err; } Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } /** * @private * @param order {Order} * @return {object} */ _buildPurchaseData(order) { // Build the description const description = this._buildRemark(order); // Create the purchasing data return { buytype: null, cart_price: order.price, id: order.item.id, ItemID: order.item.id, ItemName: order.item.name, ItemNameCN: order.item.name, ItemNick: '微店', // Weidian ItemPic: order.item.imageUrl, ItemURL: window.location.href, LocalFreight: order.shipping, promotionid: null, PropID: null, quantity: 1, remark: description, sku_id: null, sku_num: null, }; } /** * @private * @param order {Order} * @returns {string|null} */ _buildRemark(order) { const descriptionParts = []; if (order.item.model !== null) descriptionParts.push(`Model: ${order.item.model}`); if (order.item.color !== null) descriptionParts.push(`Color: ${order.item.color}`); if (order.item.size !== null) descriptionParts.push(`Size: ${order.item.size}`); if (order.item.other.length !== 0) descriptionParts.push(`${order.item.other}`); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } } /** * @param agentSelection * @returns {*} */ const getAgent = (agentSelection) => { switch (agentSelection) { case 'basetao': return new BaseTao(); case 'cssbuy': return new CSSBuy(); case 'superbuy': return new SuperBuy(); case 'wegobuy': return new WeGoBuy(); case 'ytaopal': return new Ytaopal(); case 'pandabuy': return new PandaBuy(); default: throw new Error(`Agent '${agentSelection}' is not implemented`); } }; class Store1688 { /** * @param $document * @param window */ attach($document, window) { elementReady('.order-button-wrapper > .order-button-children > .order-button-children-list').then((element) => { $(element).prepend(this._buildButton($document, window)); }); } /** * @param hostname {string} * @returns {boolean} */ supports(hostname) { return hostname.includes('1688.com'); } /** * @private * @param $document * @param window */ _buildButton($document, window) { // Force someone to select an agent if (GM_config.get('agentSelection') === 'empty') { GM_config.open(); return Snackbar('Please select what agent you use'); } // Get the agent related to our config const agent = getAgent(GM_config.get('agentSelection')); // Create button const $button = $(``); $button.on('click', async () => { // Disable button to prevent double clicks and show clear message $button.attr('disabled', true).text('Processing...'); // Try to build and send the order try { await agent.send(this._buildOrder($document, window)); } catch (err) { $button.attr('disabled', false).text(`Add to ${agent.name}`); return Snackbar(err); } $button.attr('disabled', false).text(`Add to ${agent.name}`); // Success, tell the user return Snackbar('Item has been added, be sure to double check it'); }); return $('
').append($button); } /** * @private * @param $document * @param window * @return {Shop} */ _buildShop($document, window) { const id = window.__GLOBAL_DATA.offerBaseInfo.sellerUserId; const name = window.__GLOBAL_DATA.offerBaseInfo.sellerLoginId; const url = new URL(window.__GLOBAL_DATA.offerBaseInfo.sellerWinportUrl, window.location).toString(); return new Shop(id, name, url); } /** * @private * @param $document * @param window * @return {Item} */ _buildItem($document, window) { // Build item information const id = window.__GLOBAL_DATA.tempModel.offerId; const name = removeWhitespaces(window.__GLOBAL_DATA.tempModel.offerTitle); // Build image information const imageUrl = new URL(window.__GLOBAL_DATA.images[0].size310x310ImageURI, window.location).toString(); // Retrieve the dynamic selected item const skus = this._processSku($document); return new Item(id, name, imageUrl, null, null, null, skus); } /** * @private * @param $document * @return {Number} */ _buildPrice($document) { return Number(removeWhitespaces($document.find('.order-price-wrapper .total-price .value').text())); } /** * @private * @param $document * @return {Number} */ _buildShipping($document) { return Number(removeWhitespaces($document.find('.logistics-express .logistics-express-price').text())); } /** * @private * @param $document * @param window * @return {Order} */ _buildOrder($document, window) { return new Order(this._buildShop($document, window), this._buildItem($document, window), this._buildPrice($document), this._buildShipping($document)); } /** * @private * @param $document * @return string[] */ _processSku($document) { const selectedItems = []; // Grab the module that holds the selected data const skuData = this._findModule($document.find('.pc-sku-wrapper')[0]).getSkuData(); // Grab the map we can use to find names const skuMap = skuData.skuState.skuSpecIdMap; // Parse all the selected items const selectedData = skuData.skuPannelInfo.getSubmitData().submitData; // Ensure at least one item is selected if (typeof selectedData.find((item) => item.specId !== null) === 'undefined') { throw new Error('Make sure to select at least one item'); } // Process all selections selectedData.forEach((item) => { const sku = skuMap[item.specId]; // Build the proper name let name = removeWhitespaces(sku.firstProp); if (sku.secondProp != null && sku.secondProp.length !== 0) { name = `${name} - ${removeWhitespaces(sku.secondProp)}`; } // Add it to the list with quantity selectedItems.push(`${name}: ${item.quantity}x`); }); return selectedItems; } /** * @private * @param $element * @returns {object} */ _findModule($element) { const instanceKey = Object.keys($element).find((key) => key.startsWith('__reactInternalInstance$')); const internalInstance = $element[instanceKey]; if (internalInstance == null) return null; return internalInstance.return.ref.current; } } class Enum { constructor() { this._model = ['型号', '模型', '模型', 'model', 'type']; this._colors = ['颜色', '彩色', '色', '色彩', '配色', '配色方案', 'color', 'colour', 'color scheme']; this._sizing = ['尺寸', '尺码', '型号尺寸', '大小', '浆液', '码数', '码', 'size', 'sizing']; } _arrayContains(array, query) { return array.filter((item) => query.toLowerCase().indexOf(item.toLowerCase()) !== -1).length !== 0; } isModel(item) { return this._arrayContains(this._model, item); } isColor(item) { return this._arrayContains(this._colors, item); } isSize(item) { return this._arrayContains(this._sizing, item); } } /** * @param s {string|undefined} * @returns {string} */ const capitalize = (s) => (s && s[0].toUpperCase() + s.slice(1)) || ''; const retrieveDynamicInformation = ($document, rowCss, rowTitleCss, selectedItemCss) => { // Create dynamic items let model = null; let color = null; let size = null; const others = []; // Load dynamic items $document.find(rowCss).each((key, value) => { const _enum = new Enum(); const rowTitle = $(value).find(rowTitleCss).text(); const selectedItem = $(value).find(selectedItemCss); // Check if this is model if (_enum.isModel(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Model is missing'); } model = removeWhitespaces(selectedItem.text()); return; } // Check if this is color if (_enum.isColor(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Color is missing'); } color = removeWhitespaces(selectedItem.text()); return; } // Check if this is size if (_enum.isSize(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Sizing is missing'); } size = removeWhitespaces(selectedItem.text()); return; } others.push(`${capitalize(rowTitle)}: ${removeWhitespaces(selectedItem.text())}`); }); return { model, color, size, others }; }; class TaoBao { /** * @param $document * @param window */ attach($document, window) { // Setup for item page $document.find('#detail .tb-property-x .tb-key .tb-action').after(this._buildButton($document, window)); } /** * @param hostname {string} * @returns {boolean} */ supports(hostname) { return hostname.includes('taobao.com'); } /** * @private * @param $document * @param window */ _buildButton($document, window) { // Force someone to select an agent if (GM_config.get('agentSelection') === 'empty') { GM_config.open(); return Snackbar('Please select what agent you use'); } // Get the agent related to our config const agent = getAgent(GM_config.get('agentSelection')); const $button = $(``) .css('width', '180px') .css('color', '#FFF') .css('border-color', '#F40') .css('background', '#F40') .css('cursor', 'pointer') .css('text-align', 'center') .css('font-family', '"Hiragino Sans GB","microsoft yahei",sans-serif') .css('font-size', '16px') .css('line-height', '38px') .css('border-width', '1px') .css('border-style', 'solid') .css('border-radius', '2px'); $button.on('click', async () => { // Disable button to prevent double clicks and show clear message $button.attr('disabled', true).text('Processing...'); // Try to build and send the order try { await agent.send(this._buildOrder($document, window)); } catch (err) { $button.attr('disabled', false).text(`Add to ${agent.name}`); return Snackbar(err); } $button.attr('disabled', false).text(`Add to ${agent.name}`); // Success, tell the user return Snackbar('Item has been added, be sure to double check it'); }); return $('
').append($button); } /** * @private * @param $document * @param window * @return {Shop} */ _buildShop($document, window) { const id = window.g_config.idata.shop.id; const name = window.g_config.shopName; const url = new URL(window.g_config.idata.shop.url, window.location).toString(); return new Shop(id, name, url); } /** * @private * @param $document * @param window * @return {Item} */ _buildItem($document, window) { // Build item information const id = window.g_config.idata.item.id; const name = window.g_config.idata.item.title; // Build image information const imageUrl = new URL(window.g_config.idata.item.pic, window.location).toString(); // Retrieve the dynamic selected item const { model, color, size, others } = retrieveDynamicInformation($document, '.tb-skin > .tb-prop', '.tb-property-type', '.tb-selected'); return new Item(id, name, imageUrl, model, color, size, others); } /** * @private * @param $document * @return {Number} */ _buildPrice($document) { const promoPrice = this._buildPromoPrice($document); if (promoPrice !== null) { return promoPrice; } return Number(removeWhitespaces($document.find('#J_StrPrice > .tb-rmb-num').text())); } /** * @private * @param $document * @return {Number|null} */ _buildPromoPrice($document) { const promoPrice = $document.find('#J_PromoPriceNum.tb-rmb-num').text(); if (promoPrice.length === 0) { return null; } const promoPrices = promoPrice.split(' '); if (promoPrices.length !== 0) { return Number(promoPrices.shift()); } return Number(promoPrice); } /** * @private * @param $document * @return {Number} */ _buildShipping($document) { const postageText = removeWhitespaces($document.find('#J_WlServiceInfo').first().text()); // Check for free shipping if (postageText.includes('快递 免运费')) { return 0; } // Try and get postage from text const postageMatches = postageText.match(/([\d.]+)/); // If we can't find any numbers, assume free as well, agents will fix it return postageMatches !== null ? Number(postageMatches[0]) : 0; } /** * @private * @param $document * @param window * @return {Order} */ _buildOrder($document, window) { return new Order(this._buildShop($document, window), this._buildItem($document, window), this._buildPrice($document), this._buildShipping($document)); } } class Tmall { /** * @param $document * @param window */ attach($document, window) { // Setup for item page $document.find('.tb-btn-basket.tb-btn-sku').before(this._buildButton($document, window)); } /** * @param hostname {string} * @returns {boolean} */ supports(hostname) { return hostname === 'detail.tmall.com'; } /** * @private * @param $document * @param window */ _buildButton($document, window) { // Force someone to select an agent if (GM_config.get('agentSelection') === 'empty') { GM_config.open(); return Snackbar('Please select what agent you use'); } // Get the agent related to our config const agent = getAgent(GM_config.get('agentSelection')); const $button = $(``) .css('width', '180px') .css('color', '#FFF') .css('border-color', '#F40') .css('background', '#F40') .css('cursor', 'pointer') .css('text-align', 'center') .css('font-family', '"Hiragino Sans GB","microsoft yahei",sans-serif') .css('font-size', '16px') .css('line-height', '38px') .css('border-width', '1px') .css('border-style', 'solid') .css('border-radius', '2px'); $button.on('click', async () => { // Disable button to prevent double clicks and show clear message $button.attr('disabled', true).text('Processing...'); // Try to build and send the order try { await agent.send(this._buildOrder($document, window)); } catch (err) { $button.attr('disabled', false).text(`Add to ${agent.name}`); return Snackbar(err); } $button.attr('disabled', false).text(`Add to ${agent.name}`); // Success, tell the user return Snackbar('Item has been added, be sure to double check it'); }); return $('
').append($button); } /** * @private * @param window * @return {Shop} */ _buildShop(window) { const id = window.g_config.shopId; const name = window.g_config.sellerNickName; const url = new URL(window.g_config.shopUrl, window.location).toString(); return new Shop(id, name, url); } /** * @private * @param $document * @param window * @return {Item} */ _buildItem($document, window) { // Build item information const id = window.g_config.itemId; const name = removeWhitespaces($document.find('#J_DetailMeta > div.tm-clear > div.tb-property > div > div.tb-detail-hd > h1').text()); // Build image information const imageUrl = $document.find('#J_ImgBooth').first().attr('src'); // Retrieve the dynamic selected item const { model, color, size, others } = retrieveDynamicInformation($document, '.tb-skin > .tb-sku > .tb-prop', '.tb-metatit', '.tb-selected'); return new Item(id, name, imageUrl, model, color, size, others); } /** * @private * @param $document * @return {Number} */ _buildPrice($document) { let price = Number(removeWhitespaces($document.find('.tm-price').first().text())); $document.find('.tm-price').each((key, element) => { const currentPrice = Number(removeWhitespaces(element.textContent)); if (price > currentPrice) price = currentPrice; }); return price; } /** * @private * @param $document * @return {Number} */ _buildShipping($document) { const postageText = removeWhitespaces($document.find('#J_PostageToggleCont > p > .tm-yen').first().text()); // Check for free shipping if (postageText.includes('快递 免运费')) { return 0; } // Try and get postage from text const postageMatches = postageText.match(/([\d.]+)/); // If we can't find any numbers, assume free as well, agents will fix it return postageMatches !== null ? Number(postageMatches[0]) : 0; } /** * @private * @param $document * @param window * @return {Order} */ _buildOrder($document, window) { return new Order(this._buildShop(window), this._buildItem($document, window), this._buildPrice($document), this._buildShipping($document)); } } class Weidian { /** * @param $document * @param window */ attach($document, window) { // Setup for item page $document.find('.footer-btn-container > span').add('.item-container > .sku-button').on('click', () => { // Force someone to select an agent if (GM_config.get('agentSelection') === 'empty') { alert('Please select what agent you use'); GM_config.open(); return; } this._attachFooter($document, window); this._attachFooterBuyNow($document, window); }); // Setup for storefront $document.on('mousedown', 'div.base-ct.img-wrapper', () => { // Force new tab for shopping cart (must be done using actual window and by overwriting window.API.Bus) window.API.Bus.on('onActiveSku', ((t) => window.open(`https://weidian.com/item.html?itemID=${t}&frb=open`).focus())); }); // Check if we are a focused screen (because of storefront handler) and open the cart right away if (new URLSearchParams(window.location.search).get('frb') === 'open') { $document.find('.footer-btn-container > span').click(); } } /** * @param hostname {string} * @returns {boolean} */ supports(hostname) { return hostname.includes('weidian.com'); } /** * @private * @param $document * @param window */ _attachFooter($document, window) { // Attach button the footer (buy with options or cart) elementReady('.sku-footer').then((element) => { // Only add the button if it doesn't exist if ($('#agent-button').length !== 0) { return; } // Add the agent button $(element).before(this._attachButton($document, window)); }); } /** * @private * @param $document * @param window */ _attachFooterBuyNow($document, window) { // Attach button the footer (buy now) elementReady('.login_plugin_wrapper').then((element) => { // Only add the button if it doesn't exist if ($('#agent-button').length !== 0) { return; } // Add the agent button $(element).parent().after(this._attachButton($document, window)); }); } /** * @private * @param $document * @param window */ _attachButton($document, window) { // Get the agent related to our config const agent = getAgent(GM_config.get('agentSelection')); const $button = $(``) .css('background', '#f29800') .css('color', '#FFFFFF') .css('font-size', '15px') .css('text-align', 'center') .css('padding', '15px 0') .css('width', '100%') .css('height', '100%') .css('cursor', 'pointer'); $button.on('click', async () => { // Disable button to prevent double clicks and show clear message $button.attr('disabled', true).text('Processing...'); // Try to build and send the order try { await agent.send(this._buildOrder($document, window)); } catch (err) { $button.attr('disabled', false).text(`Add to ${agent.name}`); return Snackbar(err); } $button.attr('disabled', false).text(`Add to ${agent.name}`); // Success, tell the user return Snackbar('Item has been added, be sure to double check it'); }); return $button; } /** * @private * @param $document * @return {Shop} */ _buildShop($document) { // Setup default values for variables let id = null; let name = null; let url = null; // Try and fill the variables let $shop = $document.find('.shop-toggle-header-name').first(); if ($shop.length !== 0) { name = removeWhitespaces($shop.text()); } $shop = $document.find('.item-header-logo').first(); if ($shop.length !== 0) { url = new URL($shop.attr('href'), window.location).toString(); id = url.replace(/^\D+/g, ''); name = removeWhitespaces($shop.text()); } $shop = $document.find('.shop-name-str').first(); if ($shop.length !== 0) { url = new URL($shop.parents('a').first().attr('href'), window.location).toString(); id = url.replace(/^\D+/g, ''); name = removeWhitespaces($shop.text()); } // If no shop name is defined, just set shop ID if ((name === null || name.length === 0) && id !== null) { name = id; } return new Shop(id, name, url); } /** * @private * @param $document * @param window * @return {Item} */ _buildItem($document, window) { // Build item information const id = window.location.href.match(/[?&]itemId=(\d+)/i)[1]; const name = removeWhitespaces($document.find('.item-title').first().text()); // Build image information let $itemImage = $document.find('img#skuPic'); if ($itemImage.length === 0) $itemImage = $document.find('img.item-img'); const imageUrl = $itemImage.first().attr('src'); const { model, color, size, others } = retrieveDynamicInformation($document, '.sku-content .sku-row', '.row-title', '.sku-item.selected'); return new Item(id, name, imageUrl, model, color, size, others); } /** * @private * @param $document * @return {Number} */ _buildPrice($document) { let $currentPrice = $document.find('.sku-cur-price'); if ($currentPrice.length === 0) $currentPrice = $document.find('.cur-price'); return Number(removeWhitespaces($currentPrice.first().text()).replace(/(\D+)/, '')); } /** * @private * @param $document * @return {Number} */ _buildShipping($document) { const $postageBlock = $document.find('.postage-block').first(); const postageMatches = removeWhitespaces($postageBlock.text()).match(/([\d.]+)/); // If we can't find any numbers, assume free, agents will fix it return postageMatches !== null ? Number(postageMatches[0]) : 0; } /** * @private * @param $document * @param window * @return {Order} */ _buildOrder($document, window) { return new Order(this._buildShop($document), this._buildItem($document, window), this._buildPrice($document), this._buildShipping($document)); } } class Yupoo { /** * @param $document * @param window */ attach($document, window) { // Setup for item page $document.find('.showalbumheader__tabgroup').prepend(this._buildButton($document, window)); } /** * @param hostname {string} * @returns {boolean} */ supports(hostname) { return hostname.includes('yupoo.com'); } /** * @private * @param $document * @param window */ _buildButton($document, window) { // Force someone to select an agent if (GM_config.get('agentSelection') === 'empty') { GM_config.open(); return Snackbar('Please select what agent you use'); } // Get the agent related to our config const agent = getAgent(GM_config.get('agentSelection')); const $button = $(`Add to ${agent.name}`); $button.on('click', async () => { // Disable button to prevent double clicks and show clear message $button.attr('disabled', true).text('Processing...'); // Try to build and send the order try { await agent.send(this._buildOrder($document, window)); } catch (err) { $button.attr('disabled', false).text(`Add to ${agent.name}`); return Snackbar(err); } $button.attr('disabled', false).text(`Add to ${agent.name}`); // Success, tell the user return Snackbar('Item has been added, be sure to double check it'); }); return $button; } /** * @private * @param $document * @param window * @return {Shop} */ _buildShop($document, window) { // Setup default values for variables const author = window.location.hostname.replace('.x.yupoo.com', ''); const name = $document.find('.showheader__headerTop > h1').first().text(); const url = `https://${author}.x.yupoo.com/albums`; return new Shop(author, name, url); } /** * @private * @param $document * @param window * @return {Item} */ _buildItem($document, window) { // Build item information const id = window.location.href.match(/albums\/(\d+)/i)[1]; const name = removeWhitespaces($document.find('h2 > .showalbumheader__gallerytitle').first().text()); // Build image information const $itemImage = $document.find('.showalbumheader__gallerycover > img').first(); const imageUrl = new URL($itemImage.attr('src').replace('photo.yupoo.com/', 'cdn.fashionreps.page/yupoo/'), window.location).toString(); // Ask for dynamic information const color = prompt('What color (leave blank if not needed)?'); const size = prompt('What size (leave blank if not needed)?'); return new Item(id, name, imageUrl, null, color, size, []); } /** * @private * @param $document * @return {Number} */ _buildPrice($document) { const priceHolder = $document.find('h2 > .showalbumheader__gallerytitle'); let currentPrice = '0'; // Try and find the price of the item const priceMatcher = priceHolder.text().match(/¥?(\d+)¥?/i); if (priceHolder && priceMatcher && priceMatcher.length !== 0) { currentPrice = priceHolder.text().match(/¥?(\d+)¥?/i)[1]; } return Number(removeWhitespaces(currentPrice).replace(/(\D+)/, '')); } /** * @private * @param $document * @param window * @return {Order} */ _buildOrder($document, window) { return new Order(this._buildShop($document, window), this._buildItem($document, window), this._buildPrice($document), 10); } } /** * @param hostname {string} */ function getStore(hostname) { const agents = [new Store1688(), new TaoBao(), new Tmall(), new Yupoo(), new Weidian()]; let agent = null; Object.values(agents).forEach((value) => { if (value.supports(hostname)) { agent = value; } }); return agent; } // Inject config styling GM_addStyle('div.config-dialog.config-dialog-ani { z-index: 2147483647; }'); // Setup proper settings menu GM_config.init('Settings', { agentSection: { label: 'Select your agent', type: 'section', }, agentSelection: { label: 'Your agent', type: 'select', default: 'empty', options: { empty: 'Select your agent...', basetao: 'BaseTao', cssbuy: 'CSSBuy', superbuy: 'SuperBuy', wegobuy: 'WeGoBuy', ytaopal: 'Ytaopal', pandabuy: 'PandaBuy', }, }, }); // 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 GM_XHR $.ajaxSetup({ xhr() { return new GM_XHR(); } }); // 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}`); // Check if we are on a store const store = getStore(window.location.hostname); if (store !== null) { // If we have a store handler, attach and start store.attach($(window.document), window.unsafeWindow); return; } // Check if we are on a reshipping agent const login = getLogin(window.location.hostname); if (login !== null) { // If we are on one, execute whatever needed for that login.process(window.unsafeWindow); return; } Logger.error('Unsupported website'); }());