// ==UserScript== // @name Weidian to Agent // @namespace https://www.reddit.com/user/RobotOilInc // @version 2.2.4 // @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 cssbuy.com // @connect superbuy.com // @connect ytaopal.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); } } 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) { // 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); }; /** * @param s {string|undefined} * @returns {string} */ const capitalize = (s) => (s && s[0].toUpperCase() + s.slice(1)) || ''; /** * 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'; } } /** * Trims the input text and removes all inbetween spaces as well. * * @param string {string} */ const removeWhitespaces = (string) => string.trim().replace(/\s(?=\s)/g, ''); class BaseTao { get name() { return 'BaseTao'; } /** * @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') { 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 * @param order {Order} */ async _buildPurchaseData(order) { // Get the CSRF token 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 + order.shipping, 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, }; } /** * @private * @returns {string} */ async _getCSRF() { // Grab data from BaseTao 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} * @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 SuperBuyError extends Error { constructor(message) { super(message); this.name = 'SuperBuyError'; } } class TaoCartsBuilder { /** * @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 SuperBuy { constructor() { this._builder = new TaoCartsBuilder(); } 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 TaoCartsBuilder(); } 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 Weidian { /** * @param $document */ attach($document) { // Setup for when someone presses the buy button $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; } // Attach button the the footer (buy with options or cart) elementReady('.sku-footer').then((element) => { // Only add the button if it doesn't exists if ($('#agent-button').length !== 0) { return; } // Add the agent button $(element).before(this._attachButton($document)); }); // Attach button the the footer (buy now) elementReady('.login_plugin_wrapper').then((element) => { // Only add the button if it doesn't exists if ($('#agent-button').length !== 0) { return; } // Add the agent button $(element).after(this._attachButton($document)); }); }); } /** * @private * @param $document */ _attachButton($document) { // 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)); } 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 = $shop.attr('href').replace('//weidian.com', 'https://weidian.com'); id = url.replace(/^\D+/g, ''); name = removeWhitespaces($shop.text()); } $shop = $document.find('.shop-name-str').first(); if ($shop.length !== 0) { url = $shop.parents('a').first().attr('href').replace('//weidian.com', 'https://weidian.com'); 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 * @return {Item} */ _buildItem($document) { const _enum = new Enum(); // Build item information const id = window.location.href.match(/[?&]itemId=(\d+)/i)[1]; const name = removeWhitespaces($document.find('.item-title').first().text()); const imageUrl = $document.find('img#skuPic').first().attr('src'); // Create dynamic items let model = null; let color = null; let size = null; const others = []; // Load dynamic items $document.find('.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 (_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 new Item(id, name, imageUrl, model, color, size, others); } /** * @private * @param $document * @return {Order} */ _buildOrder($document) { // Build order price const $currentPrice = $document.find('.sku-cur-price'); const price = Number(removeWhitespaces($currentPrice.first().text()).replace(/(\D+)/, '')); // Decide on shipping (if we can't find any numbers, assume free) const $postageBlock = $document.find('.postage-block').first(); const postageMatches = removeWhitespaces($postageBlock.text()).match(/([\d.]+)/); const shipping = postageMatches !== null ? Number(postageMatches[0]) : 0; return new Order(this._buildShop($document), this._buildItem($document), price, shipping); } } // 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}`); // Actually start extension (new Weidian()).attach($(window.document)); }());