Greasy Fork

Greasy Fork is available in English.

FR:ES - BaseTao

Upload Taobao and Yupoo QCs from BaseTao to Imgur

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

// ==UserScript==
// @name        FR:ES - BaseTao
// @namespace   https://www.reddit.com/user/RobotOilInc
// @author      RobotOilInc
// @version     1.0.0
// @description Upload Taobao and Yupoo QCs from BaseTao to Imgur
// @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/*
// @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/*
// @connect     self
// @connect     imgur.com
// @connect     fashionreps.tools
// @connect     127.0.0.1
// @connect     localhost
// @require     https://code.jquery.com/jquery-3.6.0.min.js
// @require     https://unpkg.com/@sentry/[email protected]/build/bundle.min.js
// @require     http://greasyfork.icu/scripts/426288-webptojpg/code/WebpToJpg.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.min.js
// @require     https://unpkg.com/[email protected]/dist/swagger-client.browser.js
// @require     https://unpkg.com/@sentry/[email protected]/build/bundle.tracing.min.js
// @grant       GM_openInTab
// @grant       GM_notification
// @grant       GM_xmlhttpRequest
// @run-at      document-end
// @icon        https://i.imgur.com/1aQAxbC.png
// ==/UserScript==

/* jshint esversion: 8 */
/* globals Sentry: false, $:false, SparkMD5:false, WebpToJpg: false, SwaggerClient: false */

const _VERSION = '1.0.0';
const _SWAGGER_DOC_URL = 'https://localhost:8000/api/doc.json';

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

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

    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';
  }
}

class Client {
  /**
   * @param client {SwaggerClient}
   * @param userHash {string}
   */
  constructor(client, userHash) {
    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) => {
      console.error('Could not check if the album exists an album', err);
      Sentry.captureException(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) => {
      console.error('Could not check if the album exists an album', err);
      Sentry.captureException(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,
        source: `BaseTao to Imgur ${_VERSION}`,
        website: element.website,
      },
    });
  }
}

class Imgur {
  constructor(clientId) {
    this.clientId = clientId;
  }

  async CreateAlbum(options) {
    let result;

    try {
      result = await $.ajax({
        url: 'https://api.imgur.com/3/album',
        headers: { Authorization: `Client-ID ${this.clientId}` },
        type: 'POST',
        data: {
          title: options.title,
          privacy: 'hidden',
          description: `Auto uploaded using BaseTao to Imgur ${_VERSION}: http://greasyfork.icu/scripts/387421-fr-es-basetao-extension`,
        },
      });
    } catch (err) {
      // Log the error somewhere
      console.error(`Could not make an album: ${err.statusText}`, err);

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

      // Tell the user that "something" is wrong
      GM_notification('Could not make an album, please try again later...', 'FR:ES - BaseTao');
      Sentry.captureException(err);
    }

    return result;
  }

  async AddImageToAlbum(base64Image, deleteHash, purchaseUrl) {
    try {
      await $.ajax({
        url: 'https://api.imgur.com/3/image',
        headers: { Authorization: `Client-ID ${this.clientId}` },
        type: 'POST',
        data: {
          album: deleteHash,
          type: 'base64',
          image: base64Image,
          description: `W2C: ${purchaseUrl}`,
        },
      });

      return true;
    } catch (err) {
      // If we uploaded too many files, tell the user
      if (err.responseJSON.data.error.code === 429) {
        GM_notification(`Imgur is telling us to slow down: ${err.responseJSON.data.error.message}`, 'FR:ES - BaseTao');
        return false;
      }

      // Log errors somewhere
      console.error(`An error happened when uploading the image: ${err.responseJSON.data.error.message}`, err);
      Sentry.captureException(err);

      return false;
    }
  }

  RemoveAlbum(deleteHash) {
    $.ajax({
      url: `https://api.imgur.com/3/album/${deleteHash}`,
      headers: { Authorization: `Client-ID ${this.clientId}` },
      type: 'DELETE',
    });
  }
}

// eslint-disable-next-line func-names
(async function () {
  // Setup Sentry for proper error logging, which will make my lives 20x easier, use XHR since fetch dies.
  Sentry.init({
    dsn: 'https://[email protected]/5786558',
    integrations: [new Sentry.Integrations.BrowserTracing()],
    transport: Sentry.Transports.XHRTransport,
    release: `basetao-${_VERSION}`,
    environment: 'production',
    tracesSampleRate: 1.0,
  });

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

  const username = $('#dropdownMenu1').text();
  if (typeof username === 'undefined' || username == null || username === '') {
    GM_notification('You need to be logged in to use this extension.', 'FR:ES - BaseTao');

    return;
  }

  // Ensure we know who triggered the error, but use the username hash
  const userHash = SparkMD5.hash(username);
  Sentry.setUser({ userHash });

  // Try to create Swagger client from our own documentation
  try {
    client = await new SwaggerClient({ url: _SWAGGER_DOC_URL });
  } catch (error) {
    GM_notification('We are unable to connect to FR:ES. Please try again later.', 'FR:ES - BaseTao');
    console.error(`We are unable to connect to FR:ES: ${error.statusText}`, error);
    Sentry.captureException(error);

    return;
  }

  const baseTao = new Client(client, userHash);

  // Create Simple ImgurClient
  const ClientIds = ['97a5f748b20b0ad', '4d01890bba4949c', 'fe9f4d770f42e53', '0999af252aaaba1', '6870f32f716ff3b', 'bf02cd90c8a4f1a', '5ed540f56122e1c'];
  const imgur = new Imgur(ClientIds[Math.floor(Math.random() * ClientIds.length)]);

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

    GM_notification('Pictures are being uploaded....', 'FR:ES - BaseTao');

    // Create the album
    const response = await imgur.CreateAlbum(element);
    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 imgur.AddImageToAlbum(cleanedData, deleteHash, element.url);
      }).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) {
      GM_notification('Error: Image upload failed. Try again later.', 'FR:ES - BaseTao');
      imgur.RemoveAlbum(deleteHash);

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

      return;
    }

    // Tell the user it was uploaded and open the album in the background
    GM_notification('Pictures have been uploaded!', 'FR:ES - BaseTao');
    GM_openInTab(AlbumLink, true);

    // Tell QC Suite about our uploaded QC's (if it's from TaoBao)
    if (element.isSupported) {
      client.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').first();
    $qcMarker.attr('title', 'You have uploaded your QC')
      .css('cursor', 'help')
      .css('color', 'green')
      .text('✓');

    // Remove all other QC markers
    $base.find('.qc-marker:not(:first-child)').remove();

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

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

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

      element.images = imageUrls;

      UploadToImgur(baseTao, 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: pointer;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 baseTao.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 baseTao.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();
  });
}());