"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 V2 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;
}
/**
* Sends a ReCaptcha V3 and polls for its response
*
* @param {Object} options Parameters for the request
* @param {string} options.action The site action you got from the source code
* @param {string} options.min_score The minimum score you want for the ReCaptcha V3
* @param {string} options.googlekey The google key from the ReCaptcha
* @param {string} options.pageurl The URL where the ReCaptcha V3 is
* @return {Promise<Captcha>} Promise for a Captcha object
*/
async decodeRecaptchaV3(options = {}) {
let startedAt = Date.now();
if (options.action === "") this._throwError("Missing action parameter");
if (options.googlekey === "")
this._throwError("Missing googlekey parameter");
if (options.pageurl === "") this._throwError("Missing pageurl parameter");
let upload_options = {
method: "userrecaptcha",
version: "v3",
action: options.action,
min_score: options.min_score || "0.3",
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;