Weidian to Agent

Adds an order directly from Weidian to your agent

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Weidian to Agent
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      2.2.8
// @description  Adds an order directly from Weidian 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=*
// @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-weidian-to-agent
// @supportURL   https://greasyfork.org/en/scripts/427774-weidian-to-agent
// @require      https://unpkg.com/[email protected]/src/logger.min.js
// @require      https://unpkg.com/[email protected]/dist/jquery.min.js
// @require      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://assets.geilicdn.com/fxxxx/favicon.ico
// ==/UserScript==

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

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

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

  // Set a timeout to remove the toast
  setTimeout(() => $toast.fadeOut('slow', () => $toast.remove()), 2000);
};

/**
 * @param s {string|undefined}
 * @returns {string}
 */
const capitalize = (s) => (s && s[0].toUpperCase() + s.slice(1)) || '';

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

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(window.location.href),
      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 SuperBuyError extends Error {
  constructor(message) {
    super(message);
    this.name = 'SuperBuyError';
  }
}

class TaoCartsBuilder {
  /**
   * @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 SuperBuy {
  constructor() {
    this._builder = new TaoCartsBuilder();
  }

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

  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 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);
      this._attachFooterBuyNow($document);
    });

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

  /**
   * @private
   * @param $document
   */
  _attachFooter($document) {
    // 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));
    });
  }

  /**
   * @private
   * @param $document
   */
  _attachFooterBuyNow($document) {
    // 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));
    });
  }

  /**
   * @private
   * @param $document
   */
  _attachButton($document) {
    // 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));
      } 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 = $shop.attr('href').replace('//weidian.com', 'https://weidian.com');
      id = url.replace(/^\D+/g, '');
      name = removeWhitespaces($shop.text());
    }

    $shop = $document.find('.shop-name-str').first();
    if ($shop.length !== 0) {
      url = $shop.parents('a').first().attr('href').replace('//weidian.com', 'https://weidian.com');
      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
   * @return {Item}
   */
  _buildItem($document) {
    const _enum = new Enum();

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

    // Create dynamic items
    let model = null;
    let color = null;
    let size = null;
    const others = [];

    // Load dynamic items
    $document.find('.sku-content .sku-row').each((key, value) => {
      const rowTitle = $(value).find('.row-title').text();
      const selectedItem = $(value).find('.sku-item.selected');

      // Check if this is model
      if (_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 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
   * @return {Order}
   */
  _buildOrder($document) {
    return new Order(this._buildShop($document), this._buildItem($document), this._buildPrice($document), this._buildShipping($document));
  }
}

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

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