Greasy Fork

Greasy Fork is available in English.

Weidian to Agent

Adds an order directly from Weidian to your agent

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Weidian to Agent
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      1.2.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-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==

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

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

  // Setup for when someone presses the buy button
  $('.footer-btn-container > span').add('.item-container > .sku-button > .sku-content').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(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');
      });

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