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.4
// @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 Order {
  constructor() {
    // Build order price
    this.price = Number(removeWhitespaces($('.sku-cur-price').text()).replace(/(\D+)/, ''));

    // Build shop information
    const $shop = $('.shop-name-str');
    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());

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

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

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

/**
 * 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.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: order.shipping,
      totalpric: order.price + 10,
      t_title: order.itemName,
      t_seller: order.shopName,
      t_img: order.itemImageUrl,
      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.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: [{
        shopSource: 'Weidian',
        shopLink: order.shopUrl,
        shopNick: order.shopName,
        shopId: order.shopId,
        goodsItems: [{
          count: 1,
          beginCount: 0,
          sku: '3780769661172',
          freight: order.shipping,
          freightServiceCharge: 0,
          goodsAddTime: Math.floor(Date.now() / 1000),
          goodsId: order.itemId,
          goodsPrifex: 'Weidian',
          goodsCode: `Weidian--${order.itemId}-${order.model}-${order.color}-${order.size}`,
          goodsLink: window.location.href,
          goodsName: order.itemName,
          goodsRemark: description,
          desc: description,
          picture: order.itemImageUrl,
          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) {
          $button.attr('disabled', false).text(`Add to ${GM_config.get('agentSelection')}`);
          return Snackbar(err);
        }

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

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