// ==UserScript== // @name Weidian to Agent // @namespace https://www.reddit.com/user/RobotOilInc // @version 1.2.2 // @description Adds an order directly from Weidian to your agent // @author RobotOilInc // @match https://weidian.com/item.html* // @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-weidian-to-agent // @supportURL https://greasyfork.org/en/scripts/427774-weidian-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 superbuy.com // @connect wegobuy.com // @run-at document-end // @icon https://assets.geilicdn.com/fxxxx/favicon.ico // @downloadURL none // ==/UserScript== // Chinese SKU names for colors const chineseForColors = ['颜色', '彩色', '色', '色彩']; // Chinese SKU names for sizing const chineseForSizing = ['尺寸', '尺码', '型号尺寸', '大小', '浆液']; // Chinese SKU names for model const chineseForModel = ['型号', '模型', '模型']; /** * Trims the input text and removes all inbetween spaces as well. * * @param string {string} */ const removeWhitespaces = (string) => string.trim().replace(/\s(?=\s)/g, ''); class Order { constructor() { // Items from Weidian this.price = Number(removeWhitespaces($('.sku-cur-price').text()).replace(/(\D+)/, '')); this.shopName = removeWhitespaces($('.shop-toggle-header-name').text()); this.itemName = removeWhitespaces($('.item-title').text()); this.imageUrl = $('img#skuPic').attr('src'); // Decide on shipping (if we can't find any numbers, assume free) const postageMatches = removeWhitespaces($('.postage-block').text()).match(/([\d.]+)/); this.shipping = postageMatches !== null ? Number(postageMatches[0]) : 0; // Create dynamic items this.model = null; this.color = null; this.size = null; // Load dynamic items $('.sku-content .sku-row').each((key, value) => { const rowTitle = $(value).find('.row-title').text(); const selectedItem = $(value).find('.sku-item.selected'); // Check if this is model if (chineseForModel.includes(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Model is missing'); } this.model = removeWhitespaces(selectedItem.text()); } // Check if this is color if (chineseForColors.includes(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Color is missing'); } this.color = removeWhitespaces(selectedItem.text()); } // Check if this is size if (chineseForSizing.includes(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Sizing is missing'); } this.size = removeWhitespaces(selectedItem.text()); } }); } } /** * Creates a SKU toast, which is shown because of Weidians CSS * * @param toast {string} */ const Snackbar = function (toast) { const $toast = $(`
${toast}
`).css('font-size', '20px'); // Append the toast to the body $('.sku-body').append($toast); // Set a timeout to remove it setTimeout(() => $toast.fadeOut('slow', () => { $toast.remove(); }), 2000); }; /** * Waits for an element satisfying selector to exist, then resolves promise with the element. * Useful for resolving race conditions. * * @param selector * @returns {Promise} */ const elementReady = function (selector) { return new Promise((resolve) => { const el = document.querySelector(selector); if (el) { resolve(el); } new MutationObserver((mutationRecords, observer) => { // Query for elements matching the specified selector Array.from(document.querySelectorAll(selector)).forEach((element) => { resolve(element); // Once we have resolved we don't need the observer anymore. observer.disconnect(); }); }).observe(document.documentElement, { childList: true, subtree: true, }); }); }; class BaseTao { /** * @returns {string} */ async getCsrf() { // Grab data required to add the order const data = await $.get('https://www.basetao.com/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('You need to be logged in on BaseTao to use this extension (CSRF).'); } // 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('You need to be logged in on BaseTao to use this extension (CSRF).'); } // Return CSRF return $data.find('input[name=csrf_test_name]').first().val(); } /** * @param order {Order} */ async send(order) { // Build some extra stuff we'll need const csrf = await this.getCsrf(); const modelNote = order.model !== null ? `Model: ${order.model}` : null; // Build the data we will send const purchaseData = { csrf_test_name: csrf, color: order.color, size: order.size, number: 1, pric: order.price, shipping: order.shipping, totalpric: order.price + 10, t_title: order.itemName, t_seller: order.shopName, t_img: order.imageUrl, t_href: window.location.href, s_url: window.location.href, buyyourself: 1, note: modelNote, site: null, }; Logger.info('Sending order to BaseTao...', purchaseData); // Do the actual call await $.ajax({ url: 'https://www.basetao.com/index/Ajax_data/buyonecart', data: purchaseData, type: 'POST', headers: { origin: 'https://www.basetao.com', referer: 'https://www.basetao.com/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') { Logger.error('Item could not be added', response); throw new Error('Item could not be added, make sure you are logged in'); } }).catch((err) => { Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } } class WeGoBuy { /** * @param host {string} */ constructor(host) { this.host = host; } /** * @param order {Order} * @returns {string|null} */ _buildDescription(order) { const descriptionParts = []; if (order.color !== null) descriptionParts.push(`Color: ${order.color}`); if (order.size !== null) descriptionParts.push(`Size: ${order.size}`); if (order.model !== null) descriptionParts.push(`Model: ${order.model}`); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } /** * @param order {Order} */ _buildPurchaseData(order) { // Build the description const description = this._buildDescription(order); // 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: '', goodsId: window.location.href, goodsLink: window.location.href, goodsName: order.itemName, goodsPrifex: 'NOCRAWLER', goodsRemark: '', guideGoodsId: '', is1111Yushou: 'no', picture: order.imageUrl, platForm: 'pc', price: order.price, priceNote: '', serviceCharge: 0, sku: order.imageUrl, spm: '', warehouseId: '1', }], }], }; } /** * @param order {Order} */ async send(order) { // Build the purchase data const purchaseData = this._buildPurchaseData(order); Logger.info('Sending order to WeGoBuy...', purchaseData); // Do the actual call await $.ajax({ url: `https://front.${this.host}/cart/add-cart`, data: JSON.stringify(purchaseData), type: 'POST', headers: { origin: `https://www.${this.host}`, referer: `https://www.${this.host}/`, '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') { Logger.error('Item could not be added', response.msg); throw new Error('Item could not be added'); } }).catch((err) => { Logger.error('An error happened when uploading the order', err); throw new Error('An error happened when adding the order'); }); } } /** * @param agentSelection * @returns {*} */ const getAgent = (agentSelection) => { switch (agentSelection) { case 'basetao': return new BaseTao(); case 'wegobuy': return new WeGoBuy('wegobuy.com'); case 'superbuy': return new WeGoBuy('superbuy.com'); default: throw new Error(`Agent '${agentSelection}' is not implemented`); } }; // 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', superbuy: 'SuperBuy', wegobuy: 'WeGoBuy', }, }, }); // Reload page if config changed GM_config.onclose = (saveFlag) => { if (saveFlag) { window.location.reload(); } }; // Register menu within GM GM_registerMenuCommand('Settings', GM_config.open); // 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}`); // Setup GM_XHR $.ajaxSetup({ xhr() { return new GM_XHR(); } }); // Setup for when someone presses the buy button $('.footer-btn-container > span').add('.item-container > .sku-button > .sku-content').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; } // Attach button the the footer elementReady('.sku-footer').then((element) => { 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...'); // Get the agent related to our config const agent = getAgent(GM_config.get('agentSelection')); // Try to build and send the order try { await agent.send(new Order()); } catch (err) { $button.attr('disabled', false).text(`Add to ${GM_config.get('agentSelection')}`); return Snackbar(err); } // Remove button once done $button.fadeOut('slow', () => $button.remove()); // Success, tell the user return Snackbar('Item has been added, be sure to double check it'); }); $(element).before($button); }); }); }());