Greasy Fork

Greasy Fork is available in English.

Weidian to Agent

Adds an order directly from Weidian to your agent

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

// ==UserScript==
// @name         Weidian to Agent
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      1.1.1
// @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-basetao
// @supportURL   http://greasyfork.icu/en/scripts/427774-weidian-to-basetao
// @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==

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

class Order {
  /**
  * @param itemName {string}
  * @param shopName {string}
  * @param price {number}
  * @param imageUrl {string}
  * @param color {string|null}
  * @param size {string|null}
  * @param model {string|null}
  */
  constructor(itemName, shopName, price, imageUrl, color, size, model) {
    this.itemName = itemName;
    this.color = color;
    this.size = size;
    this.shopName = shopName;
    this.price = price;
    this.imageUrl = imageUrl;
    this.model = model;
  }
}

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

/**
 * @returns {Order}
 */
const buildOrder = () => {
  // Items from Weidian
  const price = Number(removeWhitespaces($('.sku-cur-price').text()).replace(/(\D+)/, ''));
  const shopName = removeWhitespaces($('.shop-toggle-header-name').text());
  const itemName = removeWhitespaces($('.item-title').text());
  const imageUrl = $('img#skuPic').attr('src');

  // Create dynamic items
  let selectedColor = null;
  let selectedSize = null;
  let selectedModel = null;

  // Try and find the proper SKU rows
  $('.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');
      }

      selectedModel = removeWhitespaces(selectedItem.text());
    }

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

      selectedColor = removeWhitespaces(selectedItem.text());
    }

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

      selectedSize = removeWhitespaces(selectedItem.text());
    }
  });

  return new Order(itemName, shopName, price, imageUrl, selectedColor, selectedSize, selectedModel);
};

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.model !== null ? `Model: ${order.model}` : null;

    // Build the data we will send
    const purchaseData = {
      csrf_test_name: csrf,
      color: order.color,
      size: order.size,
      number: 1,
      pric: order.price,
      shipping: 10,
      totalpric: order.price + 10,
      t_title: order.itemName,
      t_seller: order.shopName,
      t_img: order.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');
    });
  }
}

/**
 * @param input {string}
 * @param maxLength {number} must be an integer
 * @returns {string}
 */
const truncate = function (input, maxLength) {
  function isHighSurrogate(codePoint) {
    return codePoint >= 0xd800 && codePoint <= 0xdbff;
  }

  function isLowSurrogate(codePoint) {
    return codePoint >= 0xdc00 && codePoint <= 0xdfff;
  }

  function getLength(segment) {
    if (typeof segment !== 'string') {
      throw new Error('Input must be string');
    }

    const charLength = segment.length;
    let byteLength = 0;
    let codePoint = null;
    let prevCodePoint = null;
    for (let i = 0; i < charLength; i++) {
      codePoint = segment.charCodeAt(i);
      // handle 4-byte non-BMP chars
      // low surrogate
      if (isLowSurrogate(codePoint)) {
        // when parsing previous hi-surrogate, 3 is added to byteLength
        if (prevCodePoint != null && isHighSurrogate(prevCodePoint)) {
          byteLength += 1;
        } else {
          byteLength += 3;
        }
      } else if (codePoint <= 0x7f) {
        byteLength += 1;
      } else if (codePoint >= 0x80 && codePoint <= 0x7ff) {
        byteLength += 2;
      } else if (codePoint >= 0x800 && codePoint <= 0xffff) {
        byteLength += 3;
      }
      prevCodePoint = codePoint;
    }

    return byteLength;
  }

  if (typeof input !== 'string') {
    throw new Error('Input must be string');
  }

  const charLength = input.length;
  let curByteLength = 0;
  let codePoint;
  let segment;

  for (let i = 0; i < charLength; i += 1) {
    codePoint = input.charCodeAt(i);
    segment = input[i];

    if (isHighSurrogate(codePoint) && isLowSurrogate(input.charCodeAt(i + 1))) {
      i += 1;
      segment += input[i];
    }

    curByteLength += getLength(segment);

    if (curByteLength === maxLength) {
      return input.slice(0, i + 1);
    }
    if (curByteLength > maxLength) {
      return input.slice(0, i - segment.length + 1);
    }
  }

  return input;
};

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

  /**
   * @param order {Order}
   * @returns {string|null}
   */
  _buildDescription(order) {
    const descriptionParts = [];
    if (order.color !== null) descriptionParts.push(`Color: ${order.color}`);
    if (order.size !== null) descriptionParts.push(`Size: ${order.size}`);
    if (order.model !== null) descriptionParts.push(`Model: ${order.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: [{
        shopLink: '',
        shopSource: 'NOCRAWLER',
        shopNick: '',
        shopId: '',
        goodsItems: [{
          goodsId: window.location.href,
          goodsPrifex: 'NOCRAWLER',
          sku: order.imageUrl,
          goodsCode: '',
          serviceCharge: 0,
          freightServiceCharge: 0,
          price: order.price,
          priceNote: '',
          freight: 10,
          count: 1,
          picture: order.imageUrl,
          desc: description,
          spm: '',
          platForm: 'pc',
          guideGoodsId: '',
          is1111Yushou: 'no',
          goodsName: truncate(order.itemName, 100),
          goodsLink: window.location.href,
          goodsRemark: '',
          goodsAddTime: Math.floor(Date.now() / 1000),
          beginCount: 0,
          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(); } });

  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(buildOrder());
    } catch (err) {
      $button.attr('disabled', false).text(`Add to ${GM_config.get('agentSelection')}`);
      return Snackbar(err);
    }

    // Remove button once done
    $button.fadeOut('slow', () => $button.remove());

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

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

    $('.sku-footer').before($button);
  });
}());