// ==UserScript== // @name Stores to Agent // @namespace https://www.reddit.com/user/RobotOilInc // @version 3.2.6 // @description Adds an order directly from stores to your agent // @author RobotOilInc // @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* // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @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 // @require https://unpkg.com/jquery@3.6.0/dist/jquery.min.js // @require https://greasyfork.org/scripts/401399-gm-xhr/code/GM%20XHR.js?version=938754 // @require https://greasyfork.org/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657 // @connect basetao.com // @connect cssbuy.com // @connect superbuy.com // @connect ytaopal.com // @connect wegobuy.com // @run-at document-end // @icon https://i.imgur.com/2lQXuqv.png // @downloadURL none // ==/UserScript== 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(', '); } } 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()), 2000); }; 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 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(); default: throw new Error(`Agent '${agentSelection}' is not implemented`); } }; 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)); } } /** * 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 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).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 = $(``); $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 $currentPrice = $document.find('h2 > .showalbumheader__gallerytitle'); const currentPrice = $currentPrice.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 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', { serverSection: { 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', }, }, }); // 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}`); // Get the proper store, if any const agent = getStore(window.location.hostname); if (agent === null) { Logger.error('Unsupported website'); return; } // Actually start extension agent.attach($(window.document), window.unsafeWindow); }());