// ==UserScript==
// @name Weidian to Agent
// @namespace https://www.reddit.com/user/RobotOilInc
// @version 1.2.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 http://greasyfork.icu/en/scripts/427774-weidian-to-agent
// @supportURL http://greasyfork.icu/en/scripts/427774-weidian-to-agent
// @require https://unpkg.com/[email protected]/src/logger.min.js
// @require https://unpkg.com/[email protected]/dist/jquery.min.js
// @require http://greasyfork.icu/scripts/401399-gm-xhr/code/GM%20XHR.js?version=938754
// @require http://greasyfork.icu/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
// ==/UserScript==
/**
* Creates a SKU toast, which is shown because of Weidians CSS
*
* @param toast {string}
*/
const Snackbar = function (toast) {
const $toast = $(`<div class="sku-toast">${toast}</div>`).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);
};
class Order {
/**
* @param itemName {string}
* @param shopName {string}
* @param price {number}
* @param imageUrl {string}
* @param color {string|null}
* @param size {string|null}
* @param model {string|null}
*/
constructor(itemName, shopName, price, imageUrl, color, size, model) {
this.itemName = itemName;
this.color = color;
this.size = size;
this.shopName = shopName;
this.price = price;
this.imageUrl = imageUrl;
this.model = model;
}
}
// 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, '');
/**
* @returns {Order}
*/
const buildOrder = () => {
// Items from Weidian
const price = Number(removeWhitespaces($('.sku-cur-price').text()).replace(/(\D+)/, ''));
const shopName = removeWhitespaces($('.shop-toggle-header-name').text());
const itemName = removeWhitespaces($('.item-title').text());
const imageUrl = $('img#skuPic').attr('src');
// Create dynamic items
let selectedColor = null;
let selectedSize = null;
let selectedModel = null;
// Try and find the proper SKU rows
$('.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');
}
selectedModel = removeWhitespaces(selectedItem.text());
}
// Check if this is color
if (chineseForColors.includes(rowTitle)) {
if (selectedItem.length === 0) {
throw new Error('Color is missing');
}
selectedColor = removeWhitespaces(selectedItem.text());
}
// Check if this is size
if (chineseForSizing.includes(rowTitle)) {
if (selectedItem.length === 0) {
throw new Error('Sizing is missing');
}
selectedSize = removeWhitespaces(selectedItem.text());
}
});
return new Order(itemName, shopName, price, imageUrl, selectedColor, selectedSize, selectedModel);
};
/**
* 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: 10,
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');
});
}
}
/**
* @param input {string}
* @param maxLength {number} must be an integer
* @returns {string}
*/
const truncate = function (input, maxLength) {
function isHighSurrogate(codePoint) {
return codePoint >= 0xd800 && codePoint <= 0xdbff;
}
function isLowSurrogate(codePoint) {
return codePoint >= 0xdc00 && codePoint <= 0xdfff;
}
function getLength(segment) {
if (typeof segment !== 'string') {
throw new Error('Input must be string');
}
const charLength = segment.length;
let byteLength = 0;
let codePoint = null;
let prevCodePoint = null;
for (let i = 0; i < charLength; i++) {
codePoint = segment.charCodeAt(i);
// handle 4-byte non-BMP chars
// low surrogate
if (isLowSurrogate(codePoint)) {
// when parsing previous hi-surrogate, 3 is added to byteLength
if (prevCodePoint != null && isHighSurrogate(prevCodePoint)) {
byteLength += 1;
} else {
byteLength += 3;
}
} else if (codePoint <= 0x7f) {
byteLength += 1;
} else if (codePoint >= 0x80 && codePoint <= 0x7ff) {
byteLength += 2;
} else if (codePoint >= 0x800 && codePoint <= 0xffff) {
byteLength += 3;
}
prevCodePoint = codePoint;
}
return byteLength;
}
if (typeof input !== 'string') {
throw new Error('Input must be string');
}
const charLength = input.length;
let curByteLength = 0;
let codePoint;
let segment;
for (let i = 0; i < charLength; i += 1) {
codePoint = input.charCodeAt(i);
segment = input[i];
if (isHighSurrogate(codePoint) && isLowSurrogate(input.charCodeAt(i + 1))) {
i += 1;
segment += input[i];
}
curByteLength += getLength(segment);
if (curByteLength === maxLength) {
return input.slice(0, i + 1);
}
if (curByteLength > maxLength) {
return input.slice(0, i - segment.length + 1);
}
}
return input;
};
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: [{
goodsId: window.location.href,
goodsPrifex: 'NOCRAWLER',
sku: order.imageUrl,
goodsCode: '',
serviceCharge: 0,
freightServiceCharge: 0,
price: order.price,
priceNote: '',
freight: 10,
count: 1,
picture: order.imageUrl,
desc: description,
spm: '',
platForm: 'pc',
guideGoodsId: '',
is1111Yushou: 'no',
goodsName: truncate(order.itemName, 100),
goodsLink: window.location.href,
goodsRemark: '',
goodsAddTime: Math.floor(Date.now() / 1000),
beginCount: 0,
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 = $(`<button>Add to ${GM_config.get('agentSelection')}</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(buildOrder());
} 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);
});
});
}());