# Source: two_captcha_client.js

    'use strict';
    const fs = require('fs');
    const {promisify} = require('util');

    const Captcha = require('./captcha');
    const constants = require('./constants');
    const HTTPRequest = require('./http_request');

    const baseUrl = 'https://2captcha.com/<action>.php';
    const readFileAsync = promisify(fs.readFile);

    class TwoCaptchaClient {
      /**
       * Constructor for the 2Captcha client object
       *
       * @param  {string}  key                  Your 2Captcha API key
       * @param  {Object}  [params]             Params for the client
       * @param  {number}  [params.timeout]     milliseconds before giving up on an captcha
       * @param  {number}  [params.polling]     milliseconds between polling for answer
       * @param  {Boolean} [params.throwErrors] Whether the client should throw errors or just log the errors
       * @return {TwoCaptchaClient}             The client object
       */
      constructor(key, {
        timeout = 60000,
        polling = 5000,
        throwErrors = false
      } = {}) {
        this.key = key;
        this.timeout = timeout;
        this.polling = polling;
        this.throwErrors = throwErrors;

        if (typeof(key) !== 'string') this._throwError('2Captcha key must be a string');
      }

      /**
       * Get balance from your account
       *
       * @return {Promise<float>} Account balance in USD
       */
      async balance() {
        let res = await this._request('res', 'get', {
          action: 'getbalance'
        });
        return res;
      }

      /**
       * Gets the response from a solved captcha
       *
       * @param  {string} captchaId The id of the desired captcha
       * @return {Promis<Captcha>}  A promise for the captcha
       */
      async captcha(captchaId) {
        const res = await this._request('res', 'get', {
          id: captchaId,
          action: 'get'
        });

        let decodedCaptcha = new Captcha();
        decodedCaptcha.id = captchaId;
        decodedCaptcha.apiResponse = res;
        decodedCaptcha.text = res.split('|', 2)[1];

        return decodedCaptcha;
      }

      /**
       * Sends an image captcha and polls for its response
       *
       * @param  {Object} options          Parameters for the requests
       * @param  {string} [options.base64] An already base64-coded image
       * @param  {Buffer} [options.buffer] A buffer object of a binary image
       * @param  {string} [options.path]   The path for a system-stored image
       * @param  {string} [options.url]    Url for a web-located image
       * @param  {string} [options.method] 2Captcha method of image sending. Can be either base64 or multipart
       * @return {Promise<Captcha>}        Promise for a Captcha object
       */
      async decode(options = {}) {
        const startedAt = Date.now();

        if (typeof(this.key) !== 'string') this._throwError('2Captcha key must be a string')

        let base64 = await this._loadCaptcha(options);

        let decodedCaptcha = await this._upload({ ...options, base64: base64});

        // Keep pooling untill the answer is ready
        while (!decodedCaptcha.text) {
          await this._sleep(this.polling);
          if (Date.now() - startedAt > this.timeout) {
            this._throwError('Captcha timeout');
            return;
          }
          decodedCaptcha = await this.captcha(decodedCaptcha.id);
        }

        return decodedCaptcha;
      }

      /**
       * Sends a ReCaptcha v2 and polls for its response
       *
       * @param  {Object} options           Parameters for the request
       * @param  {string} options.googlekey The google key from the ReCaptcha
       * @param  {string} options.pageurl   The URL where the ReCaptcha is
       * @return {Promise<Captcha>}         Promise for a Captcha object
       */
      async decodeRecaptchaV2(options = {}) {
        let startedAt = Date.now();

        if (options.googlekey === '') this._throwError('Missing googlekey parameter');
        if (options.pageurl === '') this._throwError('Missing pageurl parameter');

        let upload_options = {
          method: 'userrecaptcha',
          googlekey: options.googlekey,
          pageurl: options.pageurl
        };

        let decodedCaptcha = await this._upload(upload_options);

        // Keep pooling untill the answer is ready
        while (!decodedCaptcha.text) {
          await this._sleep(Math.max(this.polling, 10)); // Sleep at least 10 seconds
          if (Date.now() - startedAt > this.timeout) {
            this._throwError('Captcha timeout');
            return;
          }
          decodedCaptcha = await this.captcha(decodedCaptcha.id);
        }

        return decodedCaptcha;
      }

      /**
       * @deprecated /load.php route is returning error 500
       * Get current load from 2Captcha service
       *
       * @return {Promise<string>} Promise for an XML containing current load from
       * 2Captcha service
       */
      async load() {
        return await this._request('load', 'get');
      }

      /**
       * Loads a captcha image and converts to base64
       *
       * @param  {Object} options          The source of the image
       * @param  {string} [options.base64] An already base64-coded image
       * @param  {Buffer} [options.buffer] A buffer object of a binary image
       * @param  {string} [options.path]   The path for a system-stored image
       * @param  {string} [options.url]    Url for a web-located image
       * @return {Promise<string>}         Promise for a base64 string representation of an image
       */
      async _loadCaptcha(options = {}) {
        if (options.base64) {
          return options.base64;
        } else if (options.buffer) {
          return options.buffer.toString('base64');
        } else if (options.path) {
          let fileBinary = await readFileAsync(options.path);
          return new Buffer.from(fileBinary, 'binary').toString('base64');
        } else if (options.url) {
          let image = await HTTPRequest.openDataURL(options.url);
          return new Buffer.from(image, 'binary').toString('base64');
        } else {
          this._throwError('No image data received');
        }
      }

      /**
       * Makes a HTTP request for the 2Captcha API
       *
       * @param  {string} action   Path used in the 2Captcha api URL
       * @param  {string} method   HTTP verb to be used
       * @param  {string} payload  Body of the requisition
       * @return {Promise<string>} Promise for the response body
       */
      async _request(action, method = 'get', payload = {}) {
        let req = await HTTPRequest.request({
          url: baseUrl.replace('<action>', action),
          timeout: this.timeout,
          method: method,
          payload: { ...payload, key: this.key, soft_id: 2386 }
        });

        this._validateResponse(req);

        return req;
      }

      /**
       * Report incorrectly solved captcha for refund
       *
       * @param  {string} captchaId The id of the incorrectly solved captcha
       * @return {Promise<Boolean>} Promise for a boolean informing if the report
       * was received
       */
      async report(captchaId) {
        let res = await this._request('res', 'get', {
          action: 'reportbad',
          id: captchaId
        });
        return res === 'OK_REPORT_RECORDED';
      }

      /**
       * Blocks the code for the specified amount of time
       *
       * @param  {number} ms          The time in milliseconds to block the code
       * @return {Promise<undefined>} Promise for undefined that resolves after ms milliseconds
       */
      async _sleep(ms) {
        return new Promise(resolve => {
          setTimeout(resolve, ms)
        });
      }

      /**
       * Get usage statistics from your account
       *
       * @param  {Date} date       Date for the target day
       * @return {Promise<string>} Promise for an XML containing statistics about
       * target day
       */
      async stats(date) {
        let res = await this._request('res', 'get', {
          action: 'getstats',
          date: date.toISOString().slice(0, 10)
        });
        return res;
      }

      /**
       * Throws an Error if this.throwErrors is true. If this.throwErrors is false,
       * a warn is logged in the console.
       *
       * @param  {string} message      Message of the error
       * @return {(undefined|Boolean)} If an error wasn't thrown, returns false.
       */
      _throwError(message) {
        if (message === 'Your captcha is not solved yet.') return false;
        if (this.throwErrors) {
          throw new Error(message);
        } else {
          console.warn(message);
          return false;
        }
      }

      /**
       * Uploads a captcha for the 2Captcha API
       *
       * @param  {Object} options        Parametes for the controlling the requistion
       * @param  {string} options.base64 The base64 encoded image
       * @param  {string} options.method 2Captcha method of image sending. Can be either base64 or multipart
       * @return {Promise<Captcha>}      Promise for Captcha object containing the captcha ID
       */
      async _upload(options = {}) {
        let args = {};
        if (options.base64) args.body = options.base64;
        args.method = options.method || 'base64';

        // Merge args with any other required field
        args = { ...args, ...options};

        // Erase unecessary fields
        delete args.base64;
        delete args.buffer;
        delete args.path;
        delete args.url;

        let res = await this._request('in', 'post', args);

        this._validateResponse(res);

        let decodedCaptcha = new Captcha();
        decodedCaptcha.id = res.split('|', 2)[1];

        return decodedCaptcha;
      }

      /**
       * Checks if the response from 2Captcha is an Error. It may throw an error if
       * the class parameter throwExceptions is true. If it is false, only a warning
       * will be logged.
       *
       * @param  {string} body         Body from the 2Captcha response
       * @return {(undefined|Boolean)} Returns true if response is valid
       */
      _validateResponse(body) {
        let message;
        if (constants.errors[body]) {
          message = constants.errors[body];
        } else if (body === '' || body.toString().includes('ERROR')) {
          message = `Unknown 2Captcha error: ${body}`;
        } else {
          return true;
        }

        return this._throwError(message);
      }

    }

    module.exports = TwoCaptchaClient;