Greasy Fork

Greasy Fork is available in English.

Stores to Agent

Adds an order directly from stores to your agent

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

// ==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  http://greasyfork.icu/en/scripts/427774-stores-to-agent
// @supportURL   http://greasyfork.icu/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      http://greasyfork.icu/scripts/401399-gm-xhr/code/GM%20XHR.js?version=938754
// @require      http://greasyfork.icu/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657
// @connect      basetao.com
// @connect      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);
}());