// ==UserScript==
// @name Stores to Agent
// @namespace https://www.reddit.com/user/RobotOilInc
// @version 3.4.2
// @description Adds an order directly from stores to your agent
// @author RobotOilInc
// @match https://detail.1688.com/offer/*
// @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*
// @match https://*.pandabuy.com/*
// @match https://www.pandabuy.com/*
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_webRequest
// @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#sha256-05beef5b6fa26a30b9ec60404d31da048e67ae98da3657b4b6fa83274e428d37
// @require https://unpkg.com/jquery@3.6.0/dist/jquery.min.js#sha256-ff1523fb7389539c84c65aba19260648793bb4f5e29329d2ee8804bc37a3fe6e
// @require https://greasyfork.org/scripts/401399-gm-xhr/code/GM%20XHR.js?version=938754#sha256-12de0fb77963c67f3612374ed9c53249f84ee6b1de2615471bf1158cb8052201
// @require https://greasyfork.org/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657#sha256-229668ef83cd26ac207e9d780e2bba6658e1506ac0b23fb29dc94ae531dd31fb
// @connect basetao.com
// @connect cssbuy.com
// @connect superbuy.com
// @connect ytaopal.com
// @connect wegobuy.com
// @connect pandabuy.com
// @webRequest [{ "selector": "*thor.weidian.com/stardust/*", "action": "cancel" }]
// @run-at document-end
// @icon https://i.imgur.com/2lQXuqv.png
// @downloadURL none
// ==/UserScript==
class PandaBuyError extends Error {
constructor(message) {
super(message);
this.name = 'PandaBuyError';
}
}
const PandaBuyToken = 'PANDABUY_TOKEN';
const PandaBuyUserInfo = 'PANDABUY_USERINFO';
class PandaBuyLogin {
/**
* @param hostname {string}
* @returns {boolean}
*/
supports(hostname) {
return hostname.includes('pandabuy.com');
}
process(window) {
this._storeToken(window.localStorage);
}
/**
* @param localStorage {Storage}
* @private
*/
_storeToken(localStorage) {
// If we already have a token, don't bother
const currentToken = GM_getValue(PandaBuyToken, null);
if (currentToken !== null && currentToken.length !== 0) {
return;
}
// Don't bother with getting the token, if we aren't loggeed in yet
const userInfo = localStorage.getItem(PandaBuyUserInfo);
if (userInfo === null || userInfo.length === 0) {
return;
}
// The token should now exist
const updatedToken = localStorage.getItem(PandaBuyToken);
if (updatedToken === null || updatedToken.length === 0) {
throw new PandaBuyError('Could not retrieve token');
}
// Store it internally
GM_setValue(PandaBuyToken, updatedToken);
Logger.info('Updated the PandaBuy Authorization Token');
}
}
/**
* @param hostname {string}
*/
function getLogin(hostname) {
const agents = [new PandaBuyLogin()];
let agent = null;
Object.values(agents).forEach((value) => {
if (value.supports(hostname)) {
agent = value;
}
});
return agent;
}
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('\n');
}
}
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()), 5000);
};
/**
* 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';
}
}
/**
* 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 PandaBuy {
get name() {
return 'PandaBuy';
}
/**
* @param order {Order}
*/
async send(order) {
const token = GM_getValue(PandaBuyToken, null);
if (token === null || token.length === 0) {
throw new PandaBuyError('We do not have your PandaBuy authorization token yet. Please visit PandaBuy (and login if needed)');
}
// Build the purchase data
const purchaseData = this._buildPurchaseData(order);
Logger.info('Sending order to PandaBuy...', purchaseData);
// Do the actual call
await $.ajax({
url: 'https://www.pandabuy.com/gateway/user/cart/add',
headers: {
accept: 'application/json, text/plain, */*',
authorization: `Bearer ${token}`,
'content-type': 'application/json;charset=UTF-8',
currency: 'CNY',
lang: 'en',
},
referrer: `https://www.pandabuy.com/uniorder?text=${encodeURIComponent(window.location.href)}`,
referrerPolicy: 'strict-origin-when-cross-origin',
data: JSON.stringify(purchaseData),
dataType: 'json',
type: 'POST',
mode: 'cors',
credentials: 'include',
}).then((response) => {
if (response.code === 200 && response.msg === null) {
return;
}
Logger.error('Item could not be added', response.msg);
throw new PandaBuyError('Item could not be added');
}).catch((err) => {
// If the error is our own, just rethrow it
if (err instanceof PandaBuyError) {
throw err;
}
// Our token has expired
if (err.status === 401) {
// Reset the current token
GM_setValue(PandaBuyToken, null);
Logger.error('PandaBuy authorization token has expired');
throw new PandaBuyError('Your PandaBuy authorization token has expired. Please visit PandaBuy (or login at PandaBuy) again');
}
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 {
storeName: order.shop.name,
storeId: order.shop.id,
goodsUrl: window.location.href,
goodsName: order.item.name,
goodsAttr: description,
storageNo: 1,
goodsPrice: order.price,
goodsNum: 1,
fare: order.shipping,
remark: '|',
selected: 1,
purchaseType: 1,
goodsImg: order.item.imageUrl,
servicePrice: '0.00',
writePrice: order.price,
actPrice: order.price,
storeSource: this._buildStoreSource(),
goodsId: order.item.id,
};
}
/**
* @private
* @param order {Order}
* @returns {string|null}
*/
_buildRemark(order) {
const descriptionParts = [];
if (order.item.model !== null && order.item.model.length !== 0) descriptionParts.push(`Model: ${order.item.model}`);
if (order.item.color !== null && order.item.color.length !== 0) descriptionParts.push(`Color: ${order.item.color}`);
if (order.item.size !== null && order.item.size.length !== 0) 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;
}
/**
* @private
* @returns {string}
*/
_buildStoreSource() {
if (window.location.hostname.includes('1688.com')) {
return '1688';
}
if (window.location.hostname.includes('taobao.com')) {
return 'taobao';
}
if (window.location.hostname.includes('weidian.com')) {
return 'weidian';
}
if (window.location.hostname.includes('yupoo.com')) {
return 'yupoo';
}
if (window.location.hostname === 'detail.tmall.com') {
return 'tmall';
}
throw new PandaBuyError(`Could not determine store source ${window.location.hostname}`);
}
}
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();
case 'pandabuy':
return new PandaBuy();
default:
throw new Error(`Agent '${agentSelection}' is not implemented`);
}
};
class Store1688 {
/**
* @param $document
* @param window
*/
attach($document, window) {
elementReady('.order-button-wrapper > .order-button-children > .order-button-children-list').then((element) => {
$(element).prepend(this._buildButton($document, window));
});
}
/**
* @param hostname {string}
* @returns {boolean}
*/
supports(hostname) {
return hostname.includes('1688.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'));
// Create button
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 $('').append($button);
}
/**
* @private
* @param $document
* @param window
* @return {Shop}
*/
_buildShop($document, window) {
const id = window.__GLOBAL_DATA.offerBaseInfo.sellerUserId;
const name = window.__GLOBAL_DATA.offerBaseInfo.sellerLoginId;
const url = new URL(window.__GLOBAL_DATA.offerBaseInfo.sellerWinportUrl, 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.__GLOBAL_DATA.tempModel.offerId;
const name = removeWhitespaces(window.__GLOBAL_DATA.tempModel.offerTitle);
// Build image information
const imageUrl = new URL(window.__GLOBAL_DATA.images[0].size310x310ImageURI, window.location).toString();
// Retrieve the dynamic selected item
const skus = this._processSku($document);
return new Item(id, name, imageUrl, null, null, null, skus);
}
/**
* @private
* @param $document
* @return {Number}
*/
_buildPrice($document) {
return Number(removeWhitespaces($document.find('.order-price-wrapper .total-price .value').text()));
}
/**
* @private
* @param $document
* @return {Number}
*/
_buildShipping($document) {
return Number(removeWhitespaces($document.find('.logistics-express .logistics-express-price').text()));
}
/**
* @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));
}
/**
* @private
* @param $document
* @return string[]
*/
_processSku($document) {
const selectedItems = [];
// Grab the module that holds the selected data
const skuData = this._findModule($document.find('.pc-sku-wrapper')[0]).getSkuData();
// Grab the map we can use to find names
const skuMap = skuData.skuState.skuSpecIdMap;
// Parse all the selected items
const selectedData = skuData.skuPannelInfo.getSubmitData().submitData;
// Ensure at least one item is selected
if (typeof selectedData.find((item) => item.specId !== null) === 'undefined') {
throw new Error('Make sure to select at least one item');
}
// Process all selections
selectedData.forEach((item) => {
const sku = skuMap[item.specId];
// Build the proper name
let name = removeWhitespaces(sku.firstProp);
if (sku.secondProp != null && sku.secondProp.length !== 0) {
name = `${name} - ${removeWhitespaces(sku.secondProp)}`;
}
// Add it to the list with quantity
selectedItems.push(`${name}: ${item.quantity}x`);
});
return selectedItems;
}
/**
* @private
* @param $element
* @returns {object}
*/
_findModule($element) {
const instanceKey = Object.keys($element).find((key) => key.startsWith('__reactInternalInstance$'));
const internalInstance = $element[instanceKey];
if (internalInstance == null) return null;
return internalInstance.return.ref.current;
}
}
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));
}
}
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).parent().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 priceHolder = $document.find('h2 > .showalbumheader__gallerytitle');
let currentPrice = '0';
// Try and find the price of the item
const priceMatcher = priceHolder.text().match(/¥?(\d+)¥?/i);
if (priceHolder && priceMatcher && priceMatcher.length !== 0) {
currentPrice = priceHolder.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 Store1688(), 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', {
agentSection: {
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',
pandabuy: 'PandaBuy',
},
},
});
// 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}`);
// Check if we are on a store
const store = getStore(window.location.hostname);
if (store !== null) {
// If we have a store handler, attach and start
store.attach($(window.document), window.unsafeWindow);
return;
}
// Check if we are on a reshipping agent
const login = getLogin(window.location.hostname);
if (login !== null) {
// If we are on one, execute whatever needed for that
login.process(window.unsafeWindow);
return;
}
Logger.error('Unsupported website');
}());