Stores to Agent

Adds an order directly from stores to your agent

目前為 2022-01-07 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Stores to Agent
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      3.2.1
// @description  Adds an order directly from stores to your agent
// @author       RobotOilInc
// @match        https://weidian.com/item.html*
// @match        https://*.weidian.com/item.html*
// @match        https://*.v.weidian.com/?userid=*
// @match        https://weidian.com/?userid=*
// @match        https://*.taobao.com/item.htm*
// @match        https://*.yupoo.com/albums/*
// @match        https://detail.tmall.com/item.htm*
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @license      MIT
// @homepageURL  https://greasyfork.org/en/scripts/427774-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
// @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(', ');
  }
}

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);
};

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, '');

class BaseTao {
  get name() {
    return 'BaseTao';
  }

  /**
   * @param order {Order}
   */
  async send(order) {
    // Build the purchase data
    const purchaseData = await this._buildPurchaseData(order);

    Logger.info('Sending order to BaseTao...', purchaseData);

    // Do the actual call
    await $.ajax({
      url: 'https://www.basetao.com/index/Ajax_data/buyonecart',
      data: purchaseData,
      type: 'POST',
      headers: {
        origin: 'https://www.basetao.com',
        referer: 'https://www.basetao.com/index/selfhelporder.html',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36',
        'x-requested-with': 'XMLHttpRequest',
      },
    }).then((response) => {
      if (removeWhitespaces(response) === '1') {
        return;
      }

      Logger.error('Item could not be added', response);
      throw new BaseTaoError('Item could not be added, make sure you are logged in');
    }).catch((err) => {
      // If the error is our own, just rethrow it
      if (err instanceof BaseTaoError) {
        throw err;
      }

      Logger.error('An error happened when uploading the order', err);
      throw new Error('An error happened when adding the order');
    });
  }

  /**
   * @private
   * @param order {Order}
   */
  async _buildPurchaseData(order) {
    // Get the CSRF token
    const csrf = await this._getCSRF();

    // Build the data we will send
    return {
      csrf_test_name: csrf,
      color: order.item.color,
      size: order.item.size,
      number: 1,
      pric: order.price,
      shipping: order.shipping,
      totalpric: order.price + order.shipping,
      t_title: 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),
      site: null,
    };
  }

  /**
   * @private
   * @returns {Promise<string>}
   */
  async _getCSRF() {
    // Grab data from BaseTao
    const data = await $.get('https://www.basetao.com/index/selfhelporder.html');

    // Check if user is actually logged in
    if (data.indexOf('long time no operation ,please sign in again') !== -1) {
      throw new Error('You need to be logged in on BaseTao to use this extension (CSRF).');
    }

    // Convert into jQuery object
    const $data = $(data);

    // Get the username
    const username = $data.find('#dropdownMenu1').text();
    if (typeof username === 'undefined' || username == null || username === '') {
      throw new Error('You need to be logged in on BaseTao to use this extension (CSRF).');
    }

    // Return CSRF
    return $data.find('input[name=csrf_test_name]').first().val();
  }

  /**
   * @private
   * @param order {Order}
   * @returns {string|null}
   */
  _buildRemark(order) {
    const descriptionParts = [];
    if (order.item.model !== null) descriptionParts.push(`Model: ${order.item.model}`);
    if (order.item.other.length !== 0) descriptionParts.push(order.item.other);

    let description = null;
    if (descriptionParts.length !== 0) {
      description = descriptionParts.join(' / ');
    }

    return description;
  }
}

class CSSBuyError extends Error {
  constructor(message) {
    super(message);
    this.name = 'CSSBuyError';
  }
}

class CSSBuy {
  get name() {
    return 'CSSBuy';
  }

  /**
   * @param order {Order}
   */
  async send(order) {
    // Build the purchase data
    const purchaseData = this._buildPurchaseData(order);

    Logger.info('Sending order to CSSBuy...', purchaseData);

    // Do the actual call
    await $.ajax({
      url: 'https://www.cssbuy.com/ajax/fast_ajax.php?action=buyone',
      data: purchaseData,
      dataType: 'json',
      type: 'POST',
      headers: {
        origin: 'https://www.cssbuy.com/item.html',
        referer: 'https://www.cssbuy.com/item.html',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36',
      },
    }).then((response) => {
      if (response.ret === 0) {
        return;
      }

      Logger.error('Item could not be added', response);
      throw new CSSBuyError('Item could not be added');
    }).catch((err) => {
      // If the error is our own, just rethrow it
      if (err instanceof CSSBuyError) {
        throw err;
      }

      Logger.error('An error happened when uploading the order', err);
      throw new Error('An error happened when adding the order');
    });
  }

  /**
   * @private
   * @param order {Order}
   * @return {object}
   */
  _buildPurchaseData(order) {
    // Build the description
    const description = this._buildRemark(order);

    // Create the purchasing data
    return {
      data: {
        buynum: 1,
        shopid: order.shop.id,
        picture: order.item.imageUrl,
        defaultimg: order.item.imageUrl,
        freight: order.shipping,
        price: order.price,
        color: order.item.color,
        colorProp: null,
        size: order.item.size,
        sizeProp: null,
        usd_price: null,
        usd_freight: null,
        usd_total_price: null,
        total: order.price + order.shipping,
        buyyourself: 0,
        seller: order.shop.name,
        href: window.location.href,
        title: order.item.name,
        note: description,
        expressno: null,
        promotionCode: null,
        option: description,
      },
    };
  }

  /**
   * @private
   * @param order {Order}
   * @returns {string|null}
   */
  _buildRemark(order) {
    const descriptionParts = [];
    if (order.item.model !== null) descriptionParts.push(`Model: ${order.item.model}`);
    if (order.item.color !== null) descriptionParts.push(`Color: ${order.item.color}`);
    if (order.item.size !== null) descriptionParts.push(`Size: ${order.item.size}`);
    if (order.item.other.length !== 0) descriptionParts.push(`${order.item.other}`);

    let description = null;
    if (descriptionParts.length !== 0) {
      description = descriptionParts.join(' / ');
    }

    return description;
  }
}

class 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 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) {
    return Number(removeWhitespaces($document.find('#J_StrPrice > .tb-rmb-num').text()));
  }

  /**
   * @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));
  }
}

/**
 * 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 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();
    let imageUrl = new URL($itemImage.attr('src'), window.location);
    imageUrl = `https://cdn.imgproxify.com/image?url=${imageUrl}&referer=https://x.yupoo.com/`;

    // 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 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);
}());