Greasy Fork

Greasy Fork is available in English.

Weidian to Agent

Adds an order directly from Weidian to your agent

目前为 2021-06-12 提交的版本,查看 最新版本

// ==UserScript==
// @name         Weidian to Agent
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      1.2.5
// @description  Adds an order directly from Weidian to your agent
// @author       RobotOilInc
// @match        https://weidian.com/item.html*
// @match        https://*.weidian.com/item.html*
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @license      MIT
// @homepageURL  http://greasyfork.icu/en/scripts/427774-weidian-to-agent
// @supportURL   http://greasyfork.icu/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      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      superbuy.com
// @connect      wegobuy.com
// @run-at       document-end
// @icon         https://assets.geilicdn.com/fxxxx/favicon.ico
// ==/UserScript==

// Chinese SKU names for colors
const chineseForColors = ['颜色', '彩色', '色', '色彩', '配色', '配色方案'];

// Chinese SKU names for sizing
const chineseForSizing = ['尺寸', '尺码', '型号尺寸', '大小', '浆液', '码数', '码'];

// Chinese SKU names for model
const chineseForModel = ['型号', '模型', '模型'];

/**
 * Trims the input text and removes all inbetween spaces as well.
 *
 * @param string {string}
 */
const removeWhitespaces = (string) => string.trim().replace(/\s(?=\s)/g, '');

class Item {
  constructor() {
    // Build item information
    this._itemName = removeWhitespaces($('.item-title').text());
    this._itemId = window.location.href.match(/[?&]itemId=(\d+)/i)[1];
    this._itemImageUrl = $('img#skuPic').attr('src');

    // Create dynamic items
    this._model = null;
    this._color = null;
    this._size = null;

    // Load dynamic items
    $('.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 (chineseForModel.includes(rowTitle)) {
        if (selectedItem.length === 0) {
          throw new Error('Model is missing');
        }

        this._model = removeWhitespaces(selectedItem.text());
      }

      // Check if this is color
      if (chineseForColors.includes(rowTitle)) {
        if (selectedItem.length === 0) {
          throw new Error('Color is missing');
        }

        this._color = removeWhitespaces(selectedItem.text());
      }

      // Check if this is size
      if (chineseForSizing.includes(rowTitle)) {
        if (selectedItem.length === 0) {
          throw new Error('Sizing is missing');
        }

        this._size = removeWhitespaces(selectedItem.text());
      }
    });
  }

  get id() {
    return this._itemId;
  }

  get name() {
    return this._itemName;
  }

  get imageUrl() {
    return this._itemImageUrl;
  }

  get model() {
    return this._model;
  }

  get color() {
    return this._color;
  }

  get size() {
    return this._size;
  }
}

class Shop {
  constructor() {
    let $shop = $('.shop-toggle-header-name');
    if ($shop.length !== 0) {
      this.shopName = removeWhitespaces($shop.text());
    }

    $shop = $('.item-header-logo');
    if ($shop.length !== 0) {
      this._shopUrl = $shop.attr('href').replace('//weidian.com', 'https://weidian.com');
      this._shopId = this._shopUrl.replace(/^\D+/g, '');
      this._shopName = removeWhitespaces($shop.text());
    }

    $shop = $('.shop-name-str');
    if ($shop.length !== 0) {
      this._shopUrl = $shop.parents('a').first().attr('href').replace('//weidian.com', 'https://weidian.com');
      this._shopId = this._shopUrl.replace(/^\D+/g, '');
      this._shopName = removeWhitespaces($shop.text());
    }

    // If no shop name is defined, just set shop ID
    if ((this._shopName === null || this._shopName.length === 0) && this._shopId !== null) {
      this._shopName = this._shopId;
    }
  }

  get id() {
    return this._shopId;
  }

  get name() {
    return this._shopName;
  }

  get url() {
    return this._shopUrl;
  }
}

class Order {
  constructor() {
    // Build order price
    this._price = Number(removeWhitespaces($('.sku-cur-price').text()).replace(/(\D+)/, ''));

    // Decide on shipping (if we can't find any numbers, assume free)
    const postageMatches = removeWhitespaces($('.postage-block').text()).match(/([\d.]+)/);
    this._shipping = postageMatches !== null ? Number(postageMatches[0]) : 0;

    // Build shop information
    this.shop = new Shop();

    // Build item information
    this.item = new Item();
  }

  get price() {
    return this._price;
  }

  get shipping() {
    return this._shipping;
  }
}

/**
 * Creates a SKU toast, which is shown because of Weidians CSS
 *
 * @param toast {string}
 */
const Snackbar = function (toast) {
  const $toast = $(`<div class="sku-toast">${toast}</div>`).css('font-size', '20px');

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

  // Set a timeout to remove it
  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
 * @returns {Promise}
 */
const elementReady = function (selector) {
  return new Promise((resolve) => {
    const el = document.querySelector(selector);
    if (el) {
      resolve(el);
    }

    new MutationObserver((mutationRecords, observer) => {
      // Query for elements matching the specified selector
      Array.from(document.querySelectorAll(selector)).forEach((element) => {
        resolve(element);
        // Once we have resolved we don't need the observer anymore.
        observer.disconnect();
      });
    }).observe(document.documentElement, {
      childList: true,
      subtree: true,
    });
  });
};

class BaseTao {
  /**
   * @returns {string}
   */
  async getCsrf() {
    // Grab data required to add the order
    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();
  }

  /**
   * @param order {Order}
   */
  async send(order) {
    // Build some extra stuff we'll need
    const csrf = await this.getCsrf();
    const modelNote = order.item.model !== null ? `Model: ${order.item.model}` : null;

    // Build the data we will send
    const purchaseData = {
      csrf_test_name: csrf,
      color: order.item.color,
      size: order.item.size,
      number: 1,
      pric: order.price,
      shipping: order.shipping,
      totalpric: order.price + 10,
      t_title: order.item.name,
      t_seller: order.shop.name,
      t_img: order.item.imageUrl,
      t_href: window.location.href,
      s_url: window.location.href,
      buyyourself: 1,
      note: modelNote,
      site: null,
    };

    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') {
        Logger.error('Item could not be added', response);
        throw new Error('Item could not be added, make sure you are logged in');
      }
    }).catch((err) => {
      Logger.error('An error happened when uploading the order', err);
      throw new Error('An error happened when adding the order');
    });
  }
}

class WeGoBuy {
  /**
   * @param host {string}
   */
  constructor(host) {
    this.host = host;
  }

  /**
   * @param order {Order}
   * @returns {string|null}
   */
  _buildDescription(order) {
    const descriptionParts = [];
    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.model !== null) descriptionParts.push(`Model: ${order.item.model}`);

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

    return description;
  }

  /**
   * @param order {Order}
   */
  _buildPurchaseData(order) {
    // Build the description
    const description = this._buildDescription(order);

    // Create the purchasing data
    return {
      type: 1,
      shopItems: [{
        shopSource: 'NOCRAWLER',
        goodsItems: [{
          count: 1,
          beginCount: 0,
          freight: order.shipping,
          freightServiceCharge: 0,
          goodsAddTime: Math.floor(Date.now() / 1000),
          goodsId: order.item.id,
          goodsPrifex: 'NOCRAWLER',
          goodsCode: `NOCRAWLER-${order.item.id}-${order.item.model}-${order.item.color}-${order.item.size}`,
          goodsLink: window.location.href,
          goodsName: order.item.name,
          goodsRemark: description,
          desc: description,
          picture: order.item.imageUrl,
          sku: order.item.imageUrl,
          platForm: 'pc',
          price: order.price,
          warehouseId: '1',
        }],
      }],
    };
  }

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

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

    // Do the actual call
    await $.ajax({
      url: `https://front.${this.host}/cart/add-cart`,
      data: JSON.stringify(purchaseData),
      type: 'POST',
      headers: {
        origin: `https://www.${this.host}`,
        referer: `https://www.${this.host}/`,
        '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') {
        Logger.error('Item could not be added', response.msg);
        throw new Error('Item could not be added');
      }
    }).catch((err) => {
      Logger.error('An error happened when uploading the order', err);
      throw new Error('An error happened when adding the order');
    });
  }
}

/**
 * @param agentSelection
 * @returns {*}
 */
const getAgent = (agentSelection) => {
  switch (agentSelection) {
    case 'basetao':
      return new BaseTao();
    case 'wegobuy':
      return new WeGoBuy('wegobuy.com');
    case 'superbuy':
      return new WeGoBuy('superbuy.com');
    default:
      throw new Error(`Agent '${agentSelection}' is not implemented`);
  }
};

// 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',
      superbuy: 'SuperBuy',
      wegobuy: 'WeGoBuy',
    },
  },
});

// Reload page if config changed
GM_config.onclose = (saveFlag) => { if (saveFlag) { window.location.reload(); } };

// Register menu within GM
GM_registerMenuCommand('Settings', GM_config.open);

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

  // Setup GM_XHR
  $.ajaxSetup({ xhr() { return new GM_XHR(); } });

  // Setup for when someone presses the buy button
  $('.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;
    }

    // Attach button the the footer
    elementReady('.sku-footer').then((element) => {
      const $button = $(`<button>Add to ${GM_config.get('agentSelection')}</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...');

        // Get the agent related to our config
        const agent = getAgent(GM_config.get('agentSelection'));

        // Try to build and send the order
        try {
          await agent.send(new Order());
        } catch (err) {
          Logger.error(err);
          $button.attr('disabled', false).text(`Add to ${GM_config.get('agentSelection')}`);
          return Snackbar(err);
        }

        $button.attr('disabled', false).text(`Add to ${GM_config.get('agentSelection')}`);

        // Success, tell the user
        return Snackbar('Item has been added, be sure to double check it');
      });

      $(element).before($button);
    });
  });
}());