// ==UserScript== // @name Weidian to Agent // @namespace https://www.reddit.com/user/RobotOilInc // @version 1.3.1 // @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== 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)) || ''; /** * Trims the input text and removes all inbetween spaces as well. * * @param string {string} */ const removeWhitespaces = (string) => string.trim().replace(/\s(?=\s)/g, ''); class Item { constructor() { // Hold enum definition this._enum = new Enum(); // Build item information this._itemName = removeWhitespaces($('.item-title').text()); this._itemId = window.location.href.match(/[?&]itemId=(\d+)/i)[1]; this._itemImageUrl = $('img#skuPic').attr('src'); // Create dynamic items this._model = null; this._color = null; this._size = null; this._other = []; // 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 (this._enum.isModel(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Model is missing'); } this._model = removeWhitespaces(selectedItem.text()); return; } // Check if this is color if (this._enum.isColor(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Color is missing'); } this._color = removeWhitespaces(selectedItem.text()); return; } // Check if this is size if (this._enum.isSize(rowTitle)) { if (selectedItem.length === 0) { throw new Error('Sizing is missing'); } this._size = removeWhitespaces(selectedItem.text()); return; } this._other.push(`${capitalize(rowTitle)}: ${removeWhitespaces(selectedItem.text())}`); }); } get id() { return this._itemId; } get name() { return this._itemName; } get imageUrl() { return this._itemImageUrl; } get model() { return this._model; } get color() { return this._color; } get size() { return this._size; } get other() { return this._other.join(', '); } } class Shop { constructor() { // Setup default values for variables this._shopId = null; this._shopName = null; this._shopUrl = null; // Try and fill the variables let $shop = $('.shop-toggle-header-name'); if ($shop.length !== 0) { this._shopName = removeWhitespaces($shop.text()); } $shop = $('.item-header-logo'); if ($shop.length !== 0) { this._shopUrl = $shop.attr('href').replace('//weidian.com', 'https://weidian.com'); this._shopId = this._shopUrl.replace(/^\D+/g, ''); this._shopName = removeWhitespaces($shop.text()); } $shop = $('.shop-name-str'); if ($shop.length !== 0) { this._shopUrl = $shop.parents('a').first().attr('href').replace('//weidian.com', 'https://weidian.com'); this._shopId = this._shopUrl.replace(/^\D+/g, ''); this._shopName = removeWhitespaces($shop.text()); } // If no shop name is defined, just set shop ID if ((this._shopName === null || this._shopName.length === 0) && this._shopId !== null) { this._shopName = this._shopId; } } /** * @returns {null|string} */ get id() { return this._shopId; } /** * @returns {null|string} */ get name() { return this._shopName; } /** * @returns {null|string} */ get url() { return this._shopUrl; } } class Order { constructor() { // Build order price this._price = Number(removeWhitespaces($('.sku-cur-price').text()).replace(/(\D+)/, '')); // 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; // Build shop information this.shop = new Shop(); // Build item information this.item = new Item(); } get price() { return this._price; } get shipping() { return this._shipping; } } /** * 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 { /** * @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 !== null) descriptionParts.push(order.item.other); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } /** * @private * @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(); } /** * @private * @param order {Order} */ async _buildPurchaseData(order) { // Build some extra stuff we'll need const csrf = await this._getCSRF(); // 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 + 10, t_title: order.item.name, t_seller: order.shop.name, t_img: order.item.imageUrl, t_href: window.location.href, s_url: window.location.href, buyyourself: 1, note: this._buildRemark(order), site: null, }; } /** * @param order {Order} */ async send(order) { // Build the purchase data const purchaseData = await this._buildPurchaseData(order); 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; } /** * @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 !== null) descriptionParts.push(`${order.item.other}`); let description = null; if (descriptionParts.length !== 0) { description = descriptionParts.join(' / '); } return description; } /** * @private * @param order {Order} */ _buildPurchaseData(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: [{ shopSource: 'NOCRAWLER', goodsItems: [{ count: 1, beginCount: 0, freight: order.shipping, freightServiceCharge: 0, goodsAddTime: Math.floor(Date.now() / 1000), goodsId: order.item.id, goodsPrifex: 'NOCRAWLER', goodsCode: `NOCRAWLER-${sku}`, goodsLink: window.location.href, goodsName: order.item.name, goodsRemark: description, desc: description, picture: order.item.imageUrl, sku: order.item.imageUrl, platForm: 'pc', price: order.price, 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').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) { Logger.error(err); $button.attr('disabled', false).text(`Add to ${GM_config.get('agentSelection')}`); return Snackbar(err); } $button.attr('disabled', false).text(`Add to ${GM_config.get('agentSelection')}`); // Success, tell the user return Snackbar('Item has been added, be sure to double check it'); }); $(element).before($button); }); }); }());