Greasy Fork

Greasy Fork is available in English.

FR:ES - BaseTao

Upload Taobao and Yupoo QCs from BaseTao to Imgur + QC server

当前为 2021-05-31 提交的版本,查看 最新版本

// ==UserScript==
// @name         FR:ES - BaseTao
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      0.4.2
// @description  Upload Taobao and Yupoo QCs from BaseTao to Imgur + QC server
// @author       RobotOilInc
// @match        https://www.basetao.net/index/myhome/myorder/*
// @match        https://basetao.net/index/myhome/myorder/*
// @match        https://www.basetao.com/index/myhome/myorder/*
// @match        https://basetao.com/index/myhome/myorder/*
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_openInTab
// @license      MIT
// @homepageURL  https://www.qc-server.cf/
// @supportURL   http://greasyfork.icu/en/scripts/426977-fr-es-basetao
// @include      https://www.basetao.net/index/orderphoto/itemimg/*
// @include      https://basetao.net/index/orderphoto/itemimg/*
// @include      https://www.basetao.com/index/orderphoto/itemimg/*
// @include      https://basetao.com/index/orderphoto/itemimg/*
// @require      https://unpkg.com/sweetalert2@11/dist/sweetalert2.min.js
// @require      https://unpkg.com/[email protected]/src/logger.min.js
// @require      https://unpkg.com/[email protected]/spark-md5.min.js
// @require      https://unpkg.com/[email protected]/dist/jquery.min.js
// @require      https://unpkg.com/@phamthaibaoduy/[email protected]/dist/jquery.ajax-retry.min.js
// @require      https://unpkg.com/[email protected]/dist/swagger-client.browser.min.js
// @resource     sweetalert2 https://unpkg.com/[email protected]/dist/sweetalert2.min.css
// @run-at       document-end
// @icon         https://i.imgur.com/1aQAxbC.png
// ==/UserScript==

class Client {
  /**
     * @param version {string}
     * @param client {SwaggerClient}
     * @param userHash {string}
     */
  constructor(version, client, userHash) {
    this.version = version;
    this.client = client;
    this.userHash = userHash;
  }

  /**
     * @param element
     * @returns {Promise<null|string>}
     */
  existingAlbumByOrderId(element) {
    return this.client.apis.QualityControl.hasUploaded({
      usernameHash: this.userHash,
      orderId: element.orderId,
    }).then((response) => {
      if (typeof response.body === 'undefined') {
        return null;
      }

      if (!response.body.success) {
        return null;
      }

      return response.body.albumId;
    }).catch((err) => {
      Logger.error('Could not check if the album exists an album', err);

      return '-1';
    });
  }

  /**
     * @param url {string}
     * @returns {Promise<boolean>}
     */
  exists(url) {
    return this.client.apis.QualityControl.exists({ url }).then((response) => {
      if (typeof response.body === 'undefined') {
        return null;
      }

      if (!response.body.success) {
        return null;
      }

      return response.body.exists;
    }).catch((err) => {
      Logger.error('Could not check if the album exists an album', err);

      return false;
    });
  }

  /**
     * @param element {Element}
     * @param album {string}
     */
  uploadQc(element, album) {
    return this.client.apis.QualityControl.postQualityControlCollection({}, {
      method: 'post',
      requestContentType: 'application/json',
      requestBody: {
        usernameHash: this.userHash,
        albumId: album,
        color: element.color,
        orderId: element.orderId,
        purchaseUrl: element.url,
        sizing: element.size,
        itemPrice: element.itemPrice,
        freightPrice: element.freightPrice,
        source: `BaseTao to Imgur ${this.version}`,
        website: element.website,
      },
    });
  }
}

class Element {
  constructor($element) {
    const $baseElement = $element.parents('tr').find('td[colspan=\'2\']').first();

    this.object = $element;
    this.url = $baseElement.find('.goodsname_color').first().attr('href').replace('http://', 'https://')
      .replace('?uid=1', '');
    this.title = $baseElement.find('.goodsname_color').first().text();
    this.size = $baseElement.find('.size_color_color:nth-child(2) > u').text();
    this.orderId = $element.parents('tr').prev().find('td.tdpd > span:nth-child(2)').text();
    this.itemPrice = $element.parents('tr').find('td:nth-child(2) > span').first().text();
    this.freightPrice = $element.parents('tr').find('td:nth-child(3) > span').first().text();

    const color = $baseElement.find('.size_color_color:nth-child(1) > u').text();
    if (color !== '-' && color !== 'NO') {
      this.color = color;
    }
  }

  /**
     * @returns {{color, size}}
     */
  get sizingInfo() {
    return { color: this.color, size: this.size };
  }

  /**
     * @param imageUrls {string[]}
     */
  set images(imageUrls) {
    this.imageUrls = imageUrls;
  }

  /**
     * @returns {boolean}
     */
  get isSupported() {
    return this.url.indexOf('item.taobao.com') !== -1
            || this.url.indexOf('yupoo.com') !== -1;
  }

  /**
     * @returns {string}
     */
  get website() {
    if (this.url.indexOf('item.taobao.com') !== -1) {
      return 'taobao';
    }

    if (this.url.indexOf('yupoo.com') !== -1) {
      return 'yupoo';
    }

    return 'unknown';
  }
}

/**
 * @param text {string}
 * @param type {null|('success'|'error'|'warning'|'info')}
 */
const Snackbar = function (text, type = null) {
  if (typeof type !== 'undefined' && type != null) {
    Swal.fire({
      title: text,
      icon: type,
      position: 'bottom-start',
      showConfirmButton: false,
      allowOutsideClick: false,
      backdrop: false,
      timer: 1500,
    });

    return;
  }

  Swal.fire({
    title: text,
    position: 'bottom-start',
    showConfirmButton: false,
    allowOutsideClick: false,
    backdrop: false,
    timer: 1500,
  });
};

class Imgur {
  /**
   * @param version {string}
   */
  constructor(version) {
    this.version = version;

    this.headers = {
      authorization: 'Bearer 4ad4c52d44cf7fbfe60e9efd2d18dd1c0da5a1af',
      'x-rapidapi-key': 'c8505ccb5bmsh932b65bb9b8ce4dp159afbjsn99f2c3645c12',
      'x-rapidapi-host': 'imgur-apiv3.p.rapidapi.com',
    };
  }

  /**
   * @param options
   * @returns {Promise<*|null>}
   */
  CreateAlbum(options) {
    return $.ajax({
      url: 'https://imgur-apiv3.p.rapidapi.com/3/album',
      type: 'POST',
      headers: this.headers,
      data: { title: options.title, privacy: 'hidden' },
    }).retry({ times: 3 }).catch((err) => {
      // Log the error somewhere
      Logger.error(`Could not make an album: ${err.statusText}`, err);

      // If we uploaded too fast, tell the user
      if (err.responseJSON.data.error.code === 429) {
        Snackbar(`Imgur is telling us to slow down: ${err.responseJSON.data.error.message}`, 'error');
        return;
      }

      // Tell the user that "something" is wrong
      Snackbar('Could not make an album, please try again later...', 'error');
    });
  }

  /**
   * @param base64Image {string}
   * @param deleteHash {string}
   * @returns {Promise<*|null>}
   */
  async AddImageToAlbum(base64Image, deleteHash) {
    return $.ajax({
      url: 'https://imgur-apiv3.p.rapidapi.com/3/image',
      headers: this.headers,
      type: 'POST',
      data: { album: deleteHash, type: 'base64', image: base64Image },
    }).retry({ times: 3 }).then(() => true).catch((err) => {
      // If we uploaded too many files, tell the user
      if (err.responseJSON.data.error.code === 429) {
        return false;
      }

      // Log the error otherwise
      Logger.error('An error happened when uploading the image', err);

      return false;
    });
  }

  RemoveAlbum(deleteHash) {
    $.ajax({
      url: `https://imgur-apiv3.p.rapidapi.com/3/album/${deleteHash}`,
      headers: this.headers,
      type: 'DELETE',
    }).retry({ times: 3 });
  }
}

const toDataURL = (url) => fetch(url)
  .then((response) => response.blob())
  .then((blob) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  }));

/**
 * @param base64Data {string}
 * @returns {Promise<string>}
 */
const WebpToJpg = function (base64Data) {
  return new Promise((resolve) => {
    const image = new Image();
    image.src = base64Data;

    image.onload = () => {
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');

      canvas.width = image.width;
      canvas.height = image.height;
      context.drawImage(image, 0, 0);

      resolve(canvas.toDataURL('image/jpeg'));
    };
  });
};

class Processor {
  /**
   * @param qcClient {Client}
   * @param imgurClient {Imgur}
   */
  constructor(qcClient, imgurClient) {
    this.qcClient = qcClient;
    this.imgurClient = imgurClient;
  }

  /**
   * @param element {Element}
   * @returns {Promise<void>}
   */
  async UploadToImgur(element) {
    const $processing = $('<ul><li><span style="cursor: wait;margin-left:-29px;"><img src="https://i.imgur.com/lnFQTQz.gif" alt="Processing..."></span></li></ul>');
    const $base = element.object;
    $base.after($processing).hide();

    Snackbar('Pictures are being uploaded....');

    // Create the album
    const response = await this.imgurClient.CreateAlbum(element);
    if (typeof response === 'undefined' || response == null) {
      return;
    }

    const deleteHash = response.data.deletehash;
    const albumId = response.data.id;

    const AlbumLink = `https://imgur.com/a/${albumId}`;

    // Upload all QC images
    let uploadedImages = 0;
    const promises = [];
    $.each(element.imageUrls, (key, imageUrl) => {
      // Convert to base64, since Imgur cannot access our images
      promises.push(toDataURL(imageUrl).then(async (data) => {
        // Store our base64 and if the file is WEBP, convert it to JPG
        let base64Image = data;
        if (base64Image.indexOf('image/webp') !== -1) {
          base64Image = await WebpToJpg(base64Image);
        }

        // Remove the unnecessary `data:` part
        const cleanedData = base64Image.replace(/(data:image\/.*;base64,)/, '');

        // Upload the image to the album
        return this.imgurClient.AddImageToAlbum(cleanedData, deleteHash);
      }).then((uploaded) => {
        if (uploaded === false) {
          return;
        }

        uploadedImages++;
      }));
    });

    // Wait until everything has been tried to be uploaded
    await Promise.all(promises);

    // If not all images have been uploaded, abort everything
    if (uploadedImages !== element.imageUrls.length) {
      Snackbar('Imgur is rate-limiting you, try again later.', 'error');
      this.imgurClient.RemoveAlbum(deleteHash);

      $processing.remove();
      $base.show();

      return;
    }

    // Tell the user it was uploaded and open the album in the background
    Snackbar('Pictures have been uploaded!', 'success');
    GM_openInTab(AlbumLink, true);

    // Tell QC Suite about our uploaded QC's (if it's from TaoBao)
    if (element.isSupported) {
      this.qcClient.uploadQc(element, albumId);
    }

    // Wrap the logo in a href to the new album
    const $image = $base.find('img');
    $image.wrap(`<a href='${AlbumLink}' target='_blank' title='Go to album'></a>`);
    $image.removeAttr('title');

    // Remove processing
    $processing.remove();

    // Update the marker
    const $qcMarker = $base.find('.qc-marker:not(:first-child)').first();
    $qcMarker.attr('title', 'You have uploaded your QC')
      .css('cursor', 'help')
      .css('color', 'green')
      .text('✓');

    // Remove the click handler
    $base.off();

    // Show it again
    $base.show();
  }
}

// Inject snackbar css style
GM_addStyle(GM_getResourceText('sweetalert2'));

const _SWAGGER_DOC_URL = 'https://www.qc-server.cf/api/doc.json';
const _VERSION = GM_info.script.version;

// eslint-disable-next-line func-names
(async function () {
  // Setup the logger.
  Logger.useDefaults();

  // Log the start of the script.
  Logger.info(`Starting extension, version ${_VERSION}`);

  // Get the username
  const username = $('#dropdownMenu1').text();
  if (typeof username === 'undefined' || username == null || username === '') {
    Snackbar('You need to be logged in to use this extension.');

    return;
  }

  /** @type {SwaggerClient} */
  let client;

  // Try to create Swagger client from our own documentation
  try {
    client = await new SwaggerClient({ url: _SWAGGER_DOC_URL });
  } catch (error) {
    Snackbar('We are unable to connect to FR:ES, features will be disabled.');
    Logger.error(`We are unable to connect to FR:ES: ${_SWAGGER_DOC_URL}`, error);

    return;
  }

  // Create QC and Imgur clients, use hash of username
  const qcClient = new Client(_VERSION, client, SparkMD5.hash(username));
  const imgurClient = new Imgur(_VERSION);

  // Create the processor which does the heavy lifting
  const processor = new Processor(qcClient, imgurClient);

  // Create a simple UploadHandler
  const uploadHandler = async function () {
    const $this = $(this);
    const itemUrl = $this.data('item-url');
    const element = new Element($this);

    // Go to the QC pictures URL and grab all image src's
    $.get(itemUrl, (data) => {
      if (data.indexOf('long time no operation ,please sign in again') !== -1) {
        Logger.info('No longer logged in, reloading page for user...');
        window.location.reload();

        return;
      }

      const imageUrls = [];
      $('<div/>').html(data).find('div.container.container-top60 > img').each(function () {
        imageUrls.push($(this).attr('src'));
      });

      element.images = imageUrls;

      processor.UploadToImgur(element);
    });
  };

  $('.myparcels-ul').find('span.glyphicon.glyphicon-picture').each(async function () {
    const $this = $(this);
    const element = new Element($this);

    // This plugin only works for certain websites, so check if element is supported
    if (!element.isSupported) {
      const $upload = $('<ul><li><span style="cursor: pointer;margin-left:-29px;"><img src="https://s.imgur.com/images/favicon-16x16.png" alt="Create a basic album"></span></li></ul>');
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="Not a supported URL, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">✖</span>'));
      $upload.data('item-url', $(this).parent().attr('href'));
      $upload.on('click', uploadHandler);

      $this.parents('td').first().append($upload);

      return;
    }

    const $loading = $('<ul><li><span style="cursor: wait;margin-left:-29px;"><img src="https://i.imgur.com/lnFQTQz.gif" alt="Loading..."></span></li></ul>');
    $this.parents('td').first().append($loading);

    // Define upload object
    const $upload = $('<ul><li><span class="qc-marker" style="cursor: pointer;margin-left:-29px;"><img src="https://s.imgur.com/images/favicon-16x16.png" alt="Upload your QC"></span></li></ul>');
    $upload.data('item-url', $(this).parent().attr('href'));

    // If we couldn't talk to FR:ES, assume everything is dead and use the basic uploader.
    const albumId = await qcClient.existingAlbumByOrderId(element);
    if (albumId === '-1') {
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="FR:ES returned an error, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">⚠️</span>'));
      $upload.data('item-url', $(this).parent().attr('href'));
      $upload.on('click', uploadHandler);

      $this.parents('td').first().append($upload);
      $loading.remove();

      return;
    }

    // Has anyone ever uploaded a QC, if not, show a red marker
    const exists = await qcClient.exists(element.url);
    if (!exists) {
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="No QC in database, please upload.">(!)</span>'));
      $upload.on('click', uploadHandler);

      $this.parents('td').first().append($upload);
      $loading.remove();

      return;
    }

    // Have you ever uploaded a QC? If so, link to that album
    const $image = $upload.find('img');
    if (albumId !== null && albumId !== '-1') {
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:green;font-weight: bold;" title="You have uploaded a QC">✓</span>'));
      $image.wrap(`<a href='https://imgur.com/a/${albumId}' target='_blank' title='Go to album'></a>`);
      $image.removeAttr('title');

      $this.parents('td').first().append($upload);
      $loading.remove();

      return;
    }

    // A previous QC exists, but you haven't uploaded yours yet, show orange marker
    $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:orange;font-weight: bold;" title="Your QC is not yet in the database, please upload.">(!)</span>'));
    $upload.on('click', uploadHandler);

    $this.parents('td').first().append($upload);
    $loading.remove();
  });
}());