Stores to Agent

Adds an order directly from stores to your agent

当前为 2022-05-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Stores to Agent
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      3.3.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*
// @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/[email protected]/src/logger.min.js
// @require      https://unpkg.com/[email protected]/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
// @webRequest   [{ "selector": "*thor.weidian.com/stardust/*", "action": "cancel" }]
// @run-at       document-end
// @icon         https://i.imgur.com/2lQXuqv.png
// ==/UserScript==

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 = $(`<div style="background-color:#333;border-radius:2px;bottom:50%;color:#fff;display:block;font-size:16px;left:50%;margin-left:-150px;min-width:250px;opacity:1;padding:16px;position:fixed;right:50%;text-align:center;transition:background .2s;width:300px;z-index:2147483647">${toast}</div>`);

  // Append to the body
  $('body').append($toast);

  // Set a timeout to remove the toast
  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 {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<string>}
   */
  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<string>}
   */
  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 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();
    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 id="agent-button" class="order-normal-button order-button">Add to ${agent.name}</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 $('<div class="order-button-tip-wrapper"></div>').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;
    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 = $(`<button id="agent-button">Add to ${agent.name}</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 $('<div class="tb-btn-add-agent" style="margin-top: 20px"></div>').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 = $(`<button id="agent-button">Add to ${agent.name}</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 $('<div class="tb-btn-add-agent"></div>').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).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 = $(`<button id="agent-button">Add to ${agent.name}</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 id="agent-button" class="button">Add to ${agent.name}</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 $currentPrice = $document.find('h2 > .showalbumheader__gallerytitle');
    const currentPrice = $currentPrice.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', {
  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}`);

  // Get the proper store, if any
  const agent = getStore(window.location.hostname);
  if (agent === null) {
    Logger.error('Unsupported website');

    return;
  }

  // Actually start extension
  agent.attach($(window.document), window.unsafeWindow);
}());