Greasy Fork

Greasy Fork is available in English.

FR:Reborn - Agents extension

Upload QCs from your favorite agent to Imgur + QC server

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         FR:Reborn - Agents extension
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      1.5.7
// @description  Upload QCs from your favorite agent 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/*
// @match        https://superbuy.com/order*
// @match        https://www.superbuy.com/order*
// @match        https://wegobuy.com/order*
// @match        https://www.wegobuy.com/order*
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @license      MIT
// @homepageURL  https://www.fashionreps.page/
// @supportURL   http://greasyfork.icu/en/scripts/426977-fr-reborn-agents-extension
// @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.js
// @require      https://unpkg.com/[email protected]/src/logger.js
// @require      https://unpkg.com/[email protected]/spark-md5.js
// @require      https://unpkg.com/[email protected]/dist/jquery.js
// @require      https://unpkg.com/@phamthaibaoduy/[email protected]/dist/jquery.ajax-retry.js
// @require      https://unpkg.com/@sentry/[email protected]/build/bundle.js
// @require      https://unpkg.com/@sentry/[email protected]/build/bundle.tracing.js
// @require      https://unpkg.com/[email protected]/dist/swagger-client.browser.js
// @require      http://greasyfork.icu/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657
// @resource     sweetalert2 https://unpkg.com/[email protected]/dist/sweetalert2.min.css
// @run-at       document-end
// @icon         https://i.imgur.com/mYBHjAg.png
// ==/UserScript==

// Define default toast
const Toast = Swal.mixin({
  showConfirmButton: false,
  timerProgressBar: true,
  position: 'top-end',
  timer: 4000,
  toast: true,
  didOpen: (toast) => {
    toast.addEventListener('mouseenter', Swal.stopTimer);
    toast.addEventListener('mouseleave', Swal.resumeTimer);
  },
});

/**
 * @param text {string}
 * @param type {null|('success'|'error'|'warning'|'info')}
 */
const Snackbar = function (text, type = null) {
  Toast.fire({ title: text, icon: type != null ? type : 'info' });
};

class ImgurError extends Error {
  /**
   * @param message {string}
   * @param previous {Error}
   */
  constructor(message, previous) {
    super(message);
    this.name = 'ImgurError';
    this.previous = previous;
  }
}

class ImgurSlowdownError extends ImgurError {
  constructor(message, previous) {
    super(`Imgur is telling us to slow down:\n${message}`, previous);
  }
}

// Possible sources
const SOURCE_TAOBAO = 'taobao';
const SOURCE_YUPOO = 'yupoo';
const SOURCE_WEIDIAN = 'weidian';
const SOURCE_1688 = '1688';
const SOURCE_UNKNOWN = 'unknown';

/**
 * @param url {string}
 * @returns {boolean}
 */
const isUrl = (url) => { try { return Boolean(new URL(url)); } catch (e) { return false; } };

/**
 * @param originalUrl {string}
 * @param website {string}
 * @returns {string}
 */
const cleanPurchaseUrl = (originalUrl, website) => {
  const idMatches = originalUrl.match(/[?&]id=(\d+)|[?&]itemID=(\d+)|\/albums\/(\d+)|offer\/(\d+)/i);
  const authorMatches = originalUrl.match(/https?:\/\/(\w+)\.x\.yupoo\.com/);

  if (website === SOURCE_TAOBAO && idMatches[1].length !== 0) {
    return `https://item.taobao.com/item.htm?id=${idMatches[1]}`;
  }

  if (website === SOURCE_WEIDIAN && idMatches[2].length !== 0) {
    return `https://weidian.com/item.html?itemID=${idMatches[2]}`;
  }

  if (website === SOURCE_YUPOO && idMatches[3].length !== 0 && authorMatches[1].length !== 0) {
    return `https://${authorMatches[1]}.x.yupoo.com/albums/${idMatches[3]}`;
  }

  if (website === SOURCE_1688 && idMatches[4].length !== 0) {
    return `https://detail.1688.com/offer/${idMatches[4]}.html`;
  }

  // Just return the original URL with some clean up
  return originalUrl.replace('http://', 'https://').replace('?uid=1', '').trim();
};

/**
 * @param originalUrl {string}
 * @returns {string}
 */
const determineWebsite = (originalUrl) => {
  if (originalUrl.indexOf('item.taobao.com') !== -1 || originalUrl.indexOf('detail.tmall.com') !== -1) {
    return SOURCE_TAOBAO;
  }

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

  if (originalUrl.indexOf('weidian.com') !== -1 || originalUrl.indexOf('koudai.com') !== -1) {
    return SOURCE_WEIDIAN;
  }

  if (originalUrl.indexOf('1688.com') !== -1) {
    return SOURCE_1688;
  }

  return SOURCE_UNKNOWN;
};

const removeWhitespaces = (item) => item.trim().replace(/\s(?=\s)/g, '');

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

/**
 * @param url {string}
 * @returns {Promise<string>}
 */
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 BaseTaoElement {
  constructor($element) {
    const $baseElement = $element.parents('tr').find('td[colspan=\'2\']').first();

    this.object = $element;
    this.imageUrls = [];

    this.qcImagesUrl = $element.parent().attr('href').trim();
    this.url = $baseElement.find('.goodsname_color').first().attr('href').trim();

    this.website = determineWebsite(this.url);
    this.title = truncate(removeWhitespaces($baseElement.find('.goodsname_color').first().text()), 255);

    // Item sizing (if any)
    let sizing = removeWhitespaces($baseElement.find('.size_color_color:nth-child(2) > u').text());
    sizing = (sizing !== '-' && sizing.toLowerCase() !== 'no') ? truncate(sizing, 255) : '';
    this.sizing = sizing.length !== 0 ? sizing : null;

    // Item color (if any)
    let color = removeWhitespaces($baseElement.find('.size_color_color:nth-child(1) > u').text());
    color = (color !== '-' && color.toLowerCase() !== 'no') ? truncate(color, 255) : '';
    this.color = color.length !== 0 ? color : null;

    // Item and shipping prices
    this.itemPrice = `CNY ${removeWhitespaces($element.parents('tr').find('td:nth-child(2) > span').first().text())}`;
    this.freightPrice = `CNY ${removeWhitespaces($element.parents('tr').find('td:nth-child(3) > span').first().text())}`;

    // Item weight
    const weight = removeWhitespaces($element.parents('tr').find('td:nth-child(5) > span').first().text());
    this.weight = weight.length !== 0 ? `${weight} gram` : null;

    // eslint-disable-next-line prefer-destructuring
    this.orderId = this.qcImagesUrl.match(/itemimg\/(\d+)\.html/)[1];

    // Set at a later date, if ever
    this.albumId = null;
  }

  /**
   * @return {string}
   */
  get albumUrl() {
    return `https://imgur.com/a/${this.albumId}`;
  }

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

  /**
   * @returns {string}
   */
  get purchaseUrl() {
    return cleanPurchaseUrl(this.url, this.website);
  }
}

const ImgurIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACl0lEQVQ4jX2TS0jUURSHv3v/d9TRMh0tzYrQiF7qpgijaFERRhlkxSwyiggXKdEiCjLBCi10UVirau0mCgzMVQQ96EHv1GxRWWBoKTbK2H+c+2gx6ZRIZ3U238c59/yuAFjQebQSqAeKcSj+VwINdAHn+re13hJ/4Ju4/2IziQB2K6B+JlgKQSglg0wVJF2lEhAeFseAH2HQj4ADBPUKKJ6C4nBxdRVrQ0VkBjL4aWOMuTi/nEHjkEKyVGWy42EzX8eHAYrV5M7Ot7jLETa1r+TE4F0ej39DegrpKYSnkCrRN4fWsSiYkxA4lJyEo40DzPmeQnjPWc5kb2RlaghrNNZonNFYneits0ghkqs63xK7MITpjVFVtZneD1958eA9+7JLEuA0SQoS30wkBbp5hKZD+ykpLaSxqY0zDQdIX59LXV9nEvxLMksEGNX+lEB5Hw3OOYJpqVxurWVBxWJqP9xAS4n0EpGQgP0DZMkURuLR5ATRqM/J49ep2FlGwfaFHOluI6ZjOBPHGo2whtPzk28SkmkMx8b+OrcTOOeYm5tF11g/Ezo2NbK0hguLy5n7RnMpdyvr0gsYjUcxLhkcCYJdlRtoPNdGub+McP4anNEIa2lZspPYvSGuXuvgYPg8zTmbeT7y+d9AZgUr40IKZZwhLz+b9o4Gbqtuls+ez8T9CLU1V/D9CYy1hMpyEMeyMJ6bjLOWONFVUlqIcIL8vGzKt9RR9DKDwfZvVB++REtLNaWlRQRWBDE1s5Nworq8oFr1Y3BgJJyaFmBveCOPH/Vwp+MZkUiUL33fef3qE32BYdJP5SHS5PTPVOP5uud9UK16Z7Rd9vRJb64xTlrjcM4xOjrOr4WWYN28JCzQCN4CNf3bWm/9BkZ7QCBmf01HAAAAAElFTkSuQmCC';
const ImgurLoading = 'data:image/gif;base64,R0lGODlhEAAQAPMAAP////r6+paWlr6+vnx8fIyMjOjo6NDQ0ISEhLa2tq6urvDw8MjIyODg4J6enqampiH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAEAAQAAAETBDISau9NQjCiUxDYGmdhBCFkRUlcLCFOA3oNgXsQG2HRh0EAYWDIU6MGSSAR1G4ghRa7KjIUXCog6QzpRhYiC1HILsOEuJxGcNuTyIAIfkECQoAAAAsAAAAABAAEAAABGIQSGkQmzjLQkTTWDAgRGmAgMGVhAIESxZwBUMgSyAUATYQPIBg8OIQJwLCQbJkdjAlUCA6KfU0VEmyGWgenpNfcCAoEo6SmWtBYtCukxhAwQKeQAYWYgAHNZIFKBoMCHcTEQAh+QQJCgAAACwAAAAAEAAQAAAEWhDIOZejGDNysgyDQBAIGWRGMa7jgAVq0TUj0lEDUZxArvAU0a1nAAQOrsnIA1gqCZ6AUzI4nAxJwIEgyAQUhCQsjDmUCI1jDEhlrQrFV+ksGLApWwYz41jsIwAh+QQJCgAAACwAAAAAEAAQAAAEThDISau9IIQahiCEMGhCQxkFqBLFZ0pBWhzSkYIvMLAb/OGTBII2+QExSEBjuexhVgrKAZGgqKKTGGFgBc00Np71cVsVDJVo5ydyJt/wCAAh+QQJCgAAACwAAAAAEAAQAAAEWhDISau9OAxBiBjBtRRdSRTGpRRHeJBFOKWALAXkAKQNoSwWBgFRQAA4Q5DkgOwwhCXBYTJAdAQAopVhWSgIjR1gcLLVQrQbrBV4CcwSA8l0Alo0yA8cw+9TIgAh+QQJCgAAACwAAAAAEAAQAAAEWhDISau9WA5CxAhWMDDAwXGFQR0IgQRgWRBF7JyEQgXzIC2MFkc1MQkonMbAhyQ0Y5pBg0MREA4UwwnBWGhoUIAC55DwaAcQrIXATgyzE/bwCQ2sBGZmz7dEAAA7';

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

    if (config.get('imgurApi') === 'imgur') {
      this.headers = { authorization: `Client-ID ${config.get('imgurClientId')}` };
      this.host = config.get('imgurApiHost');

      return;
    }

    if (config.get('imgurApi') === 'rapidApi') {
      this.headers = {
        authorization: `Bearer ${config.get('rapidApiBearer')}`,
        'x-rapidapi-key': config.get('rapidApiKey'),
        'x-rapidapi-host': config.get('rapidApiHost'),
      };
      this.host = config.get('rapidApiHost');

      return;
    }

    throw new Error('Invalid Imgur API has been chosen');
  }

  /**
   * @param options
   * @returns {Promise<*|null>}
   */
  async CreateAlbum(options) {
    return $.ajax({
      url: `https://${this.host}/3/album`,
      type: 'POST',
      headers: this.headers,
      data: {
        title: options.title,
        privacy: 'hidden',
        description: `Auto uploaded using FR:Reborn - ${this.agent} ${this.version}`,
      },
    }).retry({ times: 3 }).catch((err) => {
      // Check if Imgur is being a bitch
      if (typeof err.responseJSON === 'undefined') {
        this._storeRequestError(err);

        throw new ImgurError('Could not make an album, because Imgur is returning empty responses. Please try again later...', err);
      }

      // If we uploaded too many files, re-throw as proper error
      if (err.responseJSON.status === 429 || err.responseJSON.data.error.code === 429) {
        throw new ImgurSlowdownError(err.responseJSON.data.error.message, err);
      }

      this._storeRequestError(err);

      throw new ImgurError(`An error happened when creating the album the image:\n${err.responseJSON.data.error}`, err);
    });
  }

  /**
   * @param base64Image {string}
   * @param deleteHash {string}
   * @param purchaseUrl {string}
   * @returns {Promise<*|null>}
   */
  async AddBase64ImageToAlbum(base64Image, deleteHash, purchaseUrl) {
    return $.ajax({
      url: `https://${this.host}/3/image`,
      headers: this.headers,
      type: 'POST',
      data: {
        album: deleteHash,
        type: 'base64',
        image: base64Image,
        description: `W2C: ${purchaseUrl}`,
      },
    }).retry({ times: 3 }).then(() => true).catch((err) => {
      // Check if Imgur is being a bitch
      if (typeof err.responseJSON === 'undefined') {
        this._storeRequestError(err);

        throw new ImgurError('Could not upload image, as Imgur is returning empty responses. Please try again later...', err);
      }

      // If we uploaded too many files, re-throw as proper error
      if (err.responseJSON.status === 429 || err.responseJSON.data.error.code === 429) {
        throw new ImgurSlowdownError(err.responseJSON.data.error.message, err);
      }

      this._storeRequestError(err);

      throw new ImgurError(`An error happened when uploading the image:\n${err.responseJSON.data.error}`, err);
    });
  }

  /**
   * @param imageUrl {string}
   * @param deleteHash {string}
   * @param purchaseUrl {string}
   * @returns {Promise<*|null>}
   */
  async AddImageToAlbum(imageUrl, deleteHash, purchaseUrl) {
    return $.ajax({
      url: `https://${this.host}/3/image`,
      headers: this.headers,
      type: 'POST',
      data: {
        album: deleteHash,
        image: imageUrl,
        description: `W2C: ${purchaseUrl}`,
      },
    }).retry({ times: 3 }).then(() => true).catch((err) => {
      // Check if Imgur is being a bitch
      if (typeof err.responseJSON === 'undefined') {
        this._storeRequestError(err);

        throw new ImgurError('Could not upload image, as Imgur is returning empty responses. Please try again later...', err);
      }

      // If we uploaded too many files, re-throw as proper error
      if (err.responseJSON.status === 429 || err.responseJSON.data.error.code === 429) {
        throw new ImgurSlowdownError(err.responseJSON.data.error.message, err);
      }

      this._storeRequestError(err);

      throw new ImgurError(`An error happened when uploading the image:\n${err.responseJSON.data.error}`, err);
    });
  }

  /**
   * @param deleteHash {string}
   */
  RemoveAlbum(deleteHash) {
    $.ajax({ url: `https://${this.host}/3/album/${deleteHash}`, headers: this.headers, type: 'DELETE' })
      .retry({ times: 3 }).catch(() => {});
  }

  /**
   * @private
   * @param err {Error}
   */
  _storeRequestError(err) {
    Sentry.addBreadcrumb({
      category: 'Imgur', message: `Imgur returned: '${err.statusText}'`, data: err, level: Sentry.Severity.Error,
    });
  }
}

const buildSwaggerHTTPError = function (response) {
  // Build basic error (and use response as extra)
  const error = new Error(`${response.body.detail}: ${response.url}`);

  // Add status and status code
  error.status = response.body.status;
  error.statusCode = response.body.status;

  return error;
};

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

  /**
     * @param element {BaseTaoElement|WeGoBuyElement}
     * @returns {Promise<null|string>}
     */
  existingAlbumByOrderId(element) {
    const request = { usernameHash: this.userHash, orderId: element.orderId };

    return this.client.apis.QualityControl.hasUploaded(request).then((response) => {
      if (typeof response.body === 'undefined') {
        return null;
      }

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

      // Force add the album ID to the element
      element.albumId = response.body.albumId; // eslint-disable-line no-param-reassign

      return response.body.albumId;
    }).catch((reason) => {
      Logger.error(`Could not check if the album for order '${element.orderId}' exists on the QC server`, reason);

      // For some reason we couldn't fetch information, just return, server probably down or something
      if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {
        return '-1';
      }

      // Add breadcrumb with actual request we did
      Sentry.addBreadcrumb({
        category: 'Swagger',
        message: 'existingAlbumByOrderId',
        data: { request },
        level: Sentry.Severity.Debug,
      });

      // Add breadcrumb with the error
      Sentry.addBreadcrumb({
        category: 'Swagger - Error',
        message: 'existingAlbumByOrderId',
        data: { error: reason },
        level: Sentry.Severity.Error,
      });

      // Swagger HTTP error
      if (typeof reason.response !== 'undefined') {
        Sentry.captureException(buildSwaggerHTTPError(reason));

        return '-1';
      }

      Sentry.captureException(new Error(`Could not check if the album for order '${element.orderId}' exists on the QC server`));

      return '-1';
    });
  }

  /**
     * @param url {string}
     * @returns {Promise<boolean>}
     */
  exists(url) {
    const request = { url };

    return this.client.apis.QualityControl.exists(request).then((response) => {
      if (typeof response.body === 'undefined') {
        return null;
      }

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

      return response.body.exists;
    }).catch((reason) => {
      Logger.error(`Could not check if any album exists on the QC server, URL: '${url}'`, reason);

      // For some reason we couldn't fetch information, just return, server probably down or something
      if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {
        return '-1';
      }

      // Add breadcrumb with actual request we did
      Sentry.addBreadcrumb({
        category: 'Swagger',
        message: 'exists',
        data: { request },
        level: Sentry.Severity.Debug,
      });

      // Add breadcrumb with the error
      Sentry.addBreadcrumb({
        category: 'Swagger - Error',
        message: 'exists',
        data: { error: reason },
        level: Sentry.Severity.Error,
      });

      // Swagger HTTP error
      if (typeof reason.response !== 'undefined') {
        Sentry.captureException(buildSwaggerHTTPError(reason));

        return false;
      }

      Sentry.captureException(new Error(`Could not check if any album exists on the QC server, URL: '${url}'`));

      return false;
    });
  }

  /**
     * @param element {BaseTaoElement|WeGoBuyElement}
     * @param album {string}
     */
  uploadQc(element, album) {
    const request = {
      method: 'post',
      requestContentType: 'application/json',
      requestBody: {
        usernameHash: this.userHash,
        albumId: album,
        color: element.color,
        orderId: element.orderId,
        purchaseUrl: element.purchaseUrl,
        sizing: element.sizing,
        itemPrice: element.itemPrice,
        freightPrice: element.freightPrice,
        weight: element.weight,
        source: `BaseTao to Imgur ${this.version}`,
        website: element.website,
      },
    };

    return this.client.apis.QualityControl.postQualityControlCollection({}, request).catch((reason) => {
      Logger.error('Could not upload QC to the QC server', reason);

      // For some reason we couldn't fetch information, just return, server probably down or something
      if (reason.message.includes('Failed to fetch') || reason.message.includes('NetworkError when attempting to fetch resource')) {
        return;
      }

      // Add breadcrumb with actual request we did
      Sentry.addBreadcrumb({
        category: 'Swagger',
        message: 'postQualityControlCollection',
        data: { request },
        level: Sentry.Severity.Debug,
      });

      // Add breadcrumb with the error
      Sentry.addBreadcrumb({
        category: 'Swagger - Error',
        message: 'postQualityControlCollection',
        data: { error: reason },
        level: Sentry.Severity.Error,
      });

      // Swagger HTTP error
      if (typeof reason.response !== 'undefined') {
        Sentry.captureException(buildSwaggerHTTPError(reason.response));

        return;
      }

      Sentry.captureException(new Error('Could not upload QC to the QC server'));
    });
  }
}

class BaseTao {
  constructor() {
    this.setup = false;
  }

  /**
  * @param hostname {string}
  * @returns {boolean}
  */
  supports(hostname) {
    return hostname.includes('basetao.com');
  }

  /**
   * @param client {Promise<SwaggerClient>}
   * @returns {Promise<BaseTao>}
   */
  async build(client) {
    // If already build before, just return
    if (this.setup) {
      return this;
    }

    // Ensure the toast looks decent on Basetao
    GM_addStyle('.swal2-popup.swal2-toast .swal2-title {font-size: 1.5em; font-weight: bolder}');

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

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

    // Build all the clients
    this.imgurClient = new Imgur(GM_info.script.version, GM_config, 'BaseTao');
    this.qcClient = new QC(GM_info.script.version, await client, userHash);

    // Mark that this agent has been setup
    this.setup = true;

    return this;
  }

  /**
   * @return {Promise<void>}
   */
  async process() {
    if (this.setup === false) {
      throw new Error('Agent is not setup, so cannot be used');
    }

    // Make copy of the current this, so we can use it later
    const agent = this;

    // Get the container
    const $container = $('.myparcels-ul').first();
    const elementPromises = [];

    // Add button to upload pending items
    const $uploadPending = $('<button type="button" id="pending-haul" class="btn btn-sm btn-info pull-left" disabled="disabled">Upload pending QCs</button>');
    $container.find('.pull-right.seach-button').prepend($uploadPending.prop('title', 'Loading.....').css('cursor', 'wait'));

    // Add icons to all elements
    $container.find('span.glyphicon.glyphicon-picture').each(async function () {
      elementPromises.push(new Promise((resolve) => {
        agent._buildElement($(this)).then((element) => resolve(element));
      }));
    });

    // Wait for all elements to have been loaded and build pending button
    await Promise.all(elementPromises).then((elements) => {
      // Determine all missing QCs
      const missingQcs = elements.filter((element) => element.albumId === null || element.albumId === '-1');

      // Check if there is anything to upload
      if (missingQcs.length === 0) {
        $uploadPending.prop('title', 'Everything has been uploaded already').css('cursor', 'help');

        return;
      }

      // Re-enable button
      $uploadPending.prop('disabled', false).prop('title', 'Upload all pending QCs').css('cursor', 'pointer');

      // Attach click handler
      $uploadPending.on('click', () => { this._uploadPending($uploadPending, missingQcs); });
    });
  }

  /**
   * @private
   * @param $this
   * @return {Promise<BaseTaoElement>}
   */
  async _buildElement($this) {
    const element = new BaseTaoElement($this);

    // This plugin only works for certain websites, so check if element is supported
    if (element.website === SOURCE_UNKNOWN) {
      const $upload = $(`<ul><li><span style="cursor: pointer;margin-left:-29px;"><img src="${ImgurIcon}" 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.on('click', () => { this._uploadHandler(element); });

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

      return element;
    }

    const $loading = $(`<ul><li><span style="cursor: wait;margin-left:-29px;"><img src="${ImgurLoading}" 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="${ImgurIcon}" alt="Upload your QC"></span></li></ul>`);

    // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.
    const albumId = await this.qcClient.existingAlbumByOrderId(element);
    if (albumId === '-1') {
      $upload.find('span').first().html($('<span class="qc-marker" style="cursor:help;color:red;font-weight: bold;" title="FR:Reborn returned an error or could not load your album.">⚠️</span>'));

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

      return element;
    }

    // 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='${element.albumUrl}' target='_blank' title='Go to album'></a>`);
      $image.removeAttr('title');

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

      return element;
    }

    // Has anyone ever uploaded a QC, if not, show a red marker
    const exists = await this.qcClient.exists(element.purchaseUrl);
    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', () => { this._uploadHandler(element); });

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

      return element;
    }

    // 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', () => { this._uploadHandler(element); });

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

    return element;
  }

  /**
   * @private
   * @param $uploadPending
   * @param missingQcs {Array<BaseTaoElement>}
   */
  async _uploadPending($uploadPending, missingQcs) {
    // Disable button with some information
    $uploadPending.prop('disabled', true).prop('title', 'Uploading...').css('cursor', 'progress');

    // Upload all pending elements
    await missingQcs.reduce(async (promise, element) => {
      // This line will wait for the last async function to finish.
      // The first iteration uses an already resolved Promise
      // so, it will immediately continue.
      await promise;

      // If the element was uploaded in the mean time, skip it.
      if (element.albumId !== null && element.albumId !== '-1') {
        Logger.info(`Skipping element with order '${element.orderId}' as uploaded in the meantime`);
        return;
      }

      // Upload item and just wait for it to be done
      Logger.info(`Uploading element with order '${element.orderId}'`);
      await this._uploadHandler(element);
    }, Promise.resolve());
  }

  /**
   * @private
   * @param element {BaseTaoElement}
   * @returns {Promise<void>}
   */
  async _uploadToImgur(element) {
    if (this.setup === false) {
      throw new Error('Agent is not setup, so cannot be used');
    }

    if (element.imageUrls.length === 0) {
      Snackbar(`No pictures found for order ${element.orderId}, skipping,..`);
      return;
    }

    const $processing = $(`<ul><li><span style="cursor: wait;margin-left:-29px;"><img src="${ImgurLoading}" alt="Processing..."></span></li></ul>`);
    const $base = element.object.parents('td').first().find('ul:last-child').first();
    $base.after($processing).hide();

    // Start the process
    Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);

    // Temp store deleteHash
    let deleteHash;

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

      // Extract and build information needed
      deleteHash = response.data.deletehash;
      const albumId = response.data.id;

      // Upload all QC images
      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.AddBase64ImageToAlbum(cleanedData, deleteHash, element.purchaseUrl);
        }));
      });

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

      // Set albumId in element so we don't upload it again (when doing a pending haul upload)
      element.albumId = albumId; // eslint-disable-line no-param-reassign

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

      // Tell QC Suite about our uploaded QC's (if it's supported)
      if (element.website !== SOURCE_UNKNOWN) {
        this.qcClient.uploadQc(element, albumId);
      }

      // Wrap the logo in a href to the new album
      const $image = $base.find('img');
      $image.wrap(`<a href='${element.albumUrl}' 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();
    } catch (err) {
      // Remove the created album
      this.imgurClient.RemoveAlbum(deleteHash);

      // Reset the button
      $processing.remove();
      $base.show();

      // Show the error
      Snackbar(err.message, 'error');

      // If it's the slow down error, don't log it
      if (err instanceof ImgurSlowdownError) {
        return;
      }

      // Log the error
      Sentry.captureException(err);
      Logger.error(err);
    }
  }

  /**
   * @private
   * @param element {BaseTaoElement}
   * @returns {Promise<void>}
   */
  async _uploadHandler(element) {
    if (this.setup === false) {
      throw new Error('Agent is not setup, so cannot be used');
    }

    // Go to the QC pictures URL, grab all image sources and upload the element
    await $.get(element.qcImagesUrl).then(async (data) => {
      if (data.indexOf('long time no operation ,please sign in again') !== -1) {
        Snackbar('You are no longer logged in, reloading page....', 'warning');
        Logger.info('No longer logged in, reloading page for user...');
        window.location.reload();

        return null;
      }

      // Add all image urls to the element
      $('<div/>').html(data).find('div.container.container-top60 > img').each(function () {
        element.imageUrls.push($(this).attr('src'));
      });

      // Finally go and upload the order
      return this._uploadToImgur(element);
    }).catch((err) => {
      Snackbar(`Could not get all images for order ${element.orderId}`);
      Logger.error(`Could not get all images for order ${element.orderId}`, err);
    });
  }
}

class WeGoBuyElement {
  constructor($element) {
    this.object = $element;

    // Ordere details
    this.orderId = removeWhitespaces($element.find('table > tbody > tr:nth-child(1) > td:nth-child(1) > p').text());
    this.imageUrls = $element.find('.lookPic').map((key, value) => $(value).attr('href')).get();

    // Item name
    this.title = truncate(removeWhitespaces($element.find('.js-item-title').first().text()), 255);

    // Item sizing (if any)
    const sizing = removeWhitespaces($element.find('.user_orderlist_txt').text());
    this.sizing = sizing.length !== 0 ? truncate(sizing, 255) : null;

    // Item color (WeGoBuy doesn't support separation of color, so just null)
    this.color = null;

    // Item prices
    const itemPriceMatches = truncate(removeWhitespaces($element.find('tbody > tr > td:nth-child(2)').first().text()), 255).match(/([A-Z]{2})\W+([.,0-9]+)/);
    this.itemPrice = `${itemPriceMatches[1]} ${itemPriceMatches[2]}`;

    // Freight prices
    const freightPriceMatches = truncate(removeWhitespaces($element.find('tbody > tr:nth-child(1) > td:nth-child(8) > p:nth-child(2)').first().text()), 255).match(/([A-Z]{2})\W+([.,0-9]+)/);
    this.freightPrice = `${freightPriceMatches[1]} ${freightPriceMatches[2]}`;

    // Item weight
    this.weight = null;

    // Purchase details
    const possibleUrl = removeWhitespaces($element.find('.js-item-title').first().attr('href')).trim();
    this.url = isUrl(possibleUrl) ? possibleUrl : '';
    if (this.url.length !== 0) {
      this.website = determineWebsite(this.url);
    }

    // Set at a later date, if ever
    this.albumId = null;
  }

  /**
   * @return {string}
   */
  get albumUrl() {
    return `https://imgur.com/a/${this.albumId}`;
  }

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

  /**
   * @returns {string}
   */
  get purchaseUrl() {
    return cleanPurchaseUrl(this.url, this.website);
  }
}

class WeGoBuy {
  constructor() {
    this.setup = false;
  }

  /**
  * @param hostname {string}
  * @returns {boolean}
  */
  supports(hostname) {
    return hostname.includes('wegobuy.com')
        || hostname.includes('superbuy.com');
  }

  /**
   * @param client {Promise<SwaggerClient>}
   * @returns {Promise<WeGoBuy>}
   */
  async build(client) {
    // If already build before, just return
    if (this.setup) {
      return this;
    }

    // Ensure the toast looks decent on SB/WGB
    GM_addStyle('.swal2-popup.swal2-toast .swal2-title {font-size: 1em; font-weight: bolder}');

    // Get the username
    const username = (await $.get('/ajax/user-info')).data.user_name;
    if (typeof username === 'undefined' || username == null || username === '') {
      Snackbar('You need to be logged in to use this extension.');

      return this;
    }

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

    // Build all the clients
    this.imgurClient = new Imgur(GM_info.script.version, GM_config, 'WeGoBuy');
    this.qcClient = new QC(GM_info.script.version, await client, userHash);

    // Mark that this agent has been setup
    this.setup = true;

    return this;
  }

  async process() {
    if (this.setup === false) {
      throw new Error('Agent is not setup, so cannot be used');
    }

    // Make copy of the current this, so we can use it later
    const agent = this;

    // Add icons to all elements
    $('#table_list > div').each(function () { agent._buildElement($(this)); });
  }

  /**
   * @private
   * @param $this
   * @return {Promise<void>}
   */
  async _buildElement($this) {
    const element = new WeGoBuyElement($this);

    // No pictures (like rehearsal orders), or no URL like service fees, no QC options
    if (element.imageUrls.length === 0 || element.url.length === 0) {
      return;
    }

    // This plugin only works for certain websites, so check if element is supported
    if (element.website === SOURCE_UNKNOWN) {
      const $upload = $(`<div><p>FR:Reborn:</p><span style="cursor: pointer;"><img src="${ImgurIcon}" alt="Create a basic album"></span></div>`);
      $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.on('click', () => { this._uploadToImgur(element); });

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

      return;
    }

    // Define column in which to show buttons
    const $other = $this.find('tbody > tr > td:nth-child(7)').first();

    // Show simple loading animation
    const $loading = $(`<div><p>FR:Reborn:</p><span style="cursor: wait;"><img src="${ImgurLoading}" alt="Loading..."></span></div>`);
    $other.append($loading);

    // Define upload object
    const $upload = $(`<div><p>FR:Reborn:</p><span class="qc-marker" style="cursor: pointer;"><img src="${ImgurIcon}" alt="Upload your QC"></span></div>`);

    // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.
    const albumId = await this.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:Reborn 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.on('click', () => { this._uploadToImgur(element); });

      $other.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');

      $other.append($upload);
      $loading.remove();

      return;
    }

    // Has anyone ever uploaded a QC, if not, show a red marker
    const exists = await this.qcClient.exists(element.purchaseUrl);
    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', () => { this._uploadToImgur(element); });

      $other.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', () => { this._uploadToImgur(element); });

    $other.append($upload);
    $loading.remove();
  }

  /**
   * @param element {WeGoBuyElement}
   * @returns {Promise<void>}
   */
  async _uploadToImgur(element) {
    if (this.setup === false) {
      throw new Error('Agent is not setup, so cannot be used');
    }

    const $processing = $(`<div><p>FR:Reborn:</p><span style="cursor: wait;"><img src="${ImgurLoading}" alt="Processing..."></span></div>`);
    const $options = element.object.find('tbody > tr > td:nth-child(7)').first();
    const $base = $options.find('div').first();
    $base.after($processing).hide();

    // Start the process
    Snackbar(`Pictures for '${element.orderId}' are being uploaded...`);

    // Temp store deleteHash
    let deleteHash;

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

      // Extract and build information needed
      deleteHash = response.data.deletehash;
      const albumId = response.data.id;

      // Upload all QC images
      const promises = [];
      $.each(element.imageUrls, (key, imageUrl) => {
        promises.push(this.imgurClient.AddImageToAlbum(imageUrl, deleteHash, element.purchaseUrl));
      });

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

      // Set albumId in element so we don't upload it again (when doing a pending haul upload)
      element.albumId = albumId; // eslint-disable-line no-param-reassign

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

      // Tell QC Suite about our uploaded QC's (if it's supported)
      if (element.website !== SOURCE_UNKNOWN) {
        this.qcClient.uploadQc(element, albumId);
      }

      // Remove processing
      $processing.remove();
      $base.remove();

      // Add new buttons
      $options.append($('<div><p>FR:Reborn:</p>'
          + `<span class="qc-marker" style="cursor:pointer;"><a href='${element.albumUrl}' target='_blank' title='Go to album'><img src="${ImgurIcon}" alt="Go to album"></a></span>`
          + '<span class="qc-marker" style="cursor:help;margin-left:5px;color:green;font-weight: bold;" title="You have uploaded a QC">✓</span>'
          + '</div>'));

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

      // Show it again
      $base.show();
    } catch (err) {
      // Remove the created album
      this.imgurClient.RemoveAlbum(deleteHash);

      // Reset the button
      $processing.remove();
      $base.show();

      // Show the error
      Snackbar(err.message, 'error');

      // If it's the slow down error, don't log it
      if (err instanceof ImgurSlowdownError) {
        return;
      }

      // Log the error
      Sentry.captureException(err);
      Logger.error(err);
    }
  }
}

/**
 * @param hostname {string}
 */
function getAgent(hostname) {
  const agents = [new BaseTao(), new WeGoBuy()];

  let agent = null;
  Object.values(agents).forEach((value) => {
    if (value.supports(hostname)) {
      agent = value;
    }
  });

  return agent;
}

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

// Setup proper settings menu
GM_config.init('Settings', {
  serverSection: {
    label: 'QC Server settings',
    type: 'section',
  },
  swaggerDocUrl: {
    label: 'Swagger documentation URL',
    type: 'text',
    default: 'https://www.fashionreps.page/api/doc.json',
  },
  uploadSection: {
    label: 'Upload API Options',
    type: 'section',
  },
  imgurApi: {
    label: 'Select your Imgur API',
    type: 'radio',
    default: 'imgur',
    options: {
      imgur: 'Imgur API (Free)',
      rapidApi: 'RapidAPI (Freemium)',
    },
  },
  imgurSection: {
    label: 'Imgur Options',
    type: 'section',
  },
  imgurApiHost: {
    label: 'Imgur host',
    type: 'text',
    default: 'api.imgur.com',
  },
  imgurClientId: {
    label: 'Imgur Client-ID',
    type: 'text',
    default: 'e4e18b5ab582b4c',
  },
  rapidApiSection: {
    label: 'RadidAPI Options',
    type: 'section',
  },
  rapidApiHost: {
    label: 'RapidAPI host',
    type: 'text',
    default: 'imgur-apiv3.p.rapidapi.com',
  },
  rapidApiKey: {
    label: 'RapidAPI key (only needed if RapidApi select above)',
    type: 'text',
    default: '',
  },
  rapidApiBearer: {
    label: 'RapidAPI access token (only needed if RapidApi select above)',
    type: 'text',
    default: '',
  },
});

// Reload page if config changed
GM_config.onclose = (saveFlag) => {
  if (saveFlag) {
    window.location.reload();
  }
};

// Register menu within GM
GM_registerMenuCommand('Settings', GM_config.open);

// Setup Sentry for proper error logging, which will make my lives 20x easier, use XHR since fetch dies.
Sentry.init({
  dsn: 'https://[email protected]/5802425',
  tunnel: 'https://www.fashionreps.page/sentry/tunnel',
  transport: Sentry.Transports.XHRTransport,
  release: GM_info.script.version,
  defaultIntegrations: false,
  integrations: [
    new Sentry.Integrations.InboundFilters(),
    new Sentry.Integrations.FunctionToString(),
    new Sentry.Integrations.LinkedErrors(),
    new Sentry.Integrations.UserAgent(),
  ],
  environment: 'production',
  normalizeDepth: 5,
});

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

  // Get the proper agent, if any
  const agent = getAgent(window.location.hostname);
  if (agent === null) {
    Sentry.captureMessage(`Unsupported website ${window.location.hostname}`);
    Logger.error('Unsupported website');

    return;
  }

  // Finally, try to build the proper agent and process the page
  try {
    (await agent.build(new SwaggerClient({ url: GM_config.get('swaggerDocUrl') }))).process();
  } catch (error) {
    if (error.message.includes('Failed to fetch') || error.message.includes('attempting to fetch resource')) {
      Snackbar('We are unable to connect to FR:Reborn, features will be disabled.');
      Logger.error(`We are unable to connect to FR:Reborn: ${GM_config.get('swaggerDocUrl')}`, error);

      return;
    }

    Snackbar(`An unknown issue has occurred when trying to setup the agent extension: ${error.message}`);
    Logger.error('An unknown issue has occurred', error);
  }
}());