// ==UserScript==
// @name NopeCHA - Automated reCAPTCHA Solver
// @namespace http://tampermonkey.net/
// @version 1.0.4
// @description AI for Automatic reCAPTCHA Recognition
// @author You
// @require https://update.greasyfork.org/scripts/534380/1580503/UserscriptSettings.js
// @match https://www.google.com/recaptcha/api2/bframe*
// @match https://www.google.com/recaptcha/api2/anchor*
// @icon https://nopecha.com/apple-icon-72x72.png
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @connect api.nopecha.com
// @license MIT
// ==/UserScript==
const HOST = document.referrer ? document.referrer.split("/")[2] : location.origin;
const API_ENDPOINT = "https://api.nopecha.com";
const GRID_SIZES = { 1: 1, 0: 3, 2: 4 };
const POLL_TIMEOUT = 60000;
const MAX_ATTEMPTS = 30;
const settings = UserscriptSettings;
settings.define({
key: {
name: "Enter your key",
default: "",
title: "",
},
show_data: {
name: "Check API Quota",
default: "",
title: "Click to view your current API usage and reset time",
onclick: () => {
apiRequest(`${API_ENDPOINT}/status`).then(data => {
const time = `${Math.floor(data.ttl/3600)}h ${Math.floor((data.ttl%3600)/60)}m`;
console.log(`Your free quota will reset in ${time}.`);
alert(JSON.stringify(data, null, 2));
});
}
},
disabled_hosts: {
name: (current) => `${current.includes(HOST) ? 'Enable' : 'Disable'} this site`,
default: [],
title: "Add this site to the list of disabled hosts.",
onclick: (current, update) => {
if (current.includes(HOST)) {
current = current.filter(item => item !== HOST);
} else {
current.push(HOST);
}
update(current);
console.log(current);
},
},
solve_delay_time: {
name: "Delay Solving",
default: 2000,
title: "Milliseconds to delay solving.",
},
auto_open: {
name: "Auto-Open",
default: true,
title: "Automatically opens CAPTCHA challenges.",
},
auto_solve: {
name: "Auto-Solve",
default: true,
title: "Automatically solves CAPTCHA challenges.",
},
solve_delay: {
name: "Delay Solving",
default: true,
title: "Adds a delay to avoid detection.",
},
});
const eventQueue = [], eventHandlers = [];
let checkboxObserver, intersectionObserver, captchaObserver,
isRecaptchaActive = false, isCaptchaActive = false;
async function solveCaptcha(params) {
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
try {
const response = await apiRequest(API_ENDPOINT, {
method: 'POST',
data: { ...params, type: 'recaptcha' }
});
if (!response.error) return pollCaptchaResult(response.data);
if ([10, 11, 12, 15, 16, 17].includes(response.error)) {
await delay(1000);
continue;
}
throw new Error(response.message || `Error ${response.error}`);
} catch (error) {
if (attempt === MAX_ATTEMPTS-1) throw error;
await delay(1000);
}
}
}
async function pollCaptchaResult(recognitionId) {
const startTime = Date.now();
while (Date.now() - startTime < POLL_TIMEOUT) {
const response = await apiRequest(`${API_ENDPOINT}/?id=${recognitionId}`);
if (!response.error) return response;
await delay(1000);
}
throw new Error('Polling timeout');
}
async function apiRequest(url, options = {}) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
};
if (settings.get("key")) {
headers.Authorization = `Bearer ${settings.get("key")}`;
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url,
method: options.method || 'GET',
headers,
data: options.data ? JSON.stringify(options.data) : null,
responseType: 'json',
onload: response => resolve(response.response),
onerror: reject,
ontimeout: reject,
onabort: reject
});
});
}
async function loadImage(image, target, timeout = 10000) {
if (!target && !image.complete && !await new Promise(resolve => {
const timer = setTimeout(() => resolve(false), timeout);
image.addEventListener("load", () => {
clearTimeout(timer);
resolve(true);
});
})) return;
const canvas = createCanvas(
image.naturalWidth || target?.clientWidth,
image.naturalHeight || target?.clientHeight
);
canvas.getContext("2d", { willReadFrequently: true }).drawImage(image, 0, 0);
return !isCanvasEmpty(canvas) && canvas;
}
function getPixelColor(imageData, t, n, o) {
let index = (o * t + n) * 4;
return [imageData[index], imageData[index + 1], imageData[index + 2]]
}
function isImageEmpty(canvas, minThreshold = 0, maxThreshold = 230, emptyRatio = 0.99) {
const context = canvas.getContext("2d", { willReadFrequently: true });
const width = context.canvas.width;
const height = context.canvas.height;
if (width === 0 || height === 0) return true;
const imageData = context.getImageData(0, 0, width, height).data;
let emptyPixels = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const color = getPixelColor(imageData, width, x, y, 1);
const isColorBelowThreshold = color.every(value => value <= minThreshold);
const isColorAboveThreshold = color.every(value => value >= maxThreshold);
if (isColorBelowThreshold || isColorAboveThreshold) emptyPixels++;
}
}
return emptyPixels / (width * height) > emptyRatio;
}
let isSolving = false;
async function startCaptchaSolving() {
if(isSolving) return;
isSolving = true;
while (isCaptchaActive && (getCaptchaHeader() || isVerifyButtonDisabled())) {
await delay(1000);
}
while(isCaptchaActive) {
let { task, type, cells, images, waitAfterSolve } = await getCaptchaInfo();
let startTime = new Date().valueOf(), processedCells = [...cells];
type !== 1 && (images = [images[0]]);
let processedImages = await Promise.all(images.map(s => loadImage(s)));
if(type === 1) {
const validCells = [], validImages = [];
for(const [index, img] of processedImages.entries()) {
img.width !== 100 || img.height !== 100 || (validCells.push(processedCells[index]), validImages.push(img));
}
processedCells = validCells;
processedImages = validImages;
}
if(processedImages.length === 0) {
clickElement("#recaptcha-verify-button");
await delay(3000);
continue;
}
if(processedImages.some(isImageEmpty)) {
await delay(3000);
continue;
}
const gridSize = GRID_SIZES[type];
const response = await solveCaptcha({
task,
grid: `${gridSize}x${gridSize}`,
image_data: processedImages.map(canvasToBase64),
})
if(!response || "error" in response) {
console.warn("api error", response), await delay(2e3);
continue
}
const endTime = new Date().valueOf();
if(settings.get("solve_delay")) {
const delayTime = settings.get("solve_delay_time") - endTime + startTime;
delayTime > 0 && await delay(delayTime)
}
const gridWidth = type === 2 ? 4 : 3;
for(processedCells.forEach((s, x) => {
let B = s.classList.contains("rc-imageselect-tileselected"),
h = cells.indexOf(s);
response.data[x] !== B && clickElement(`tr:nth-child(${Math.floor(h/gridWidth)+1}) td:nth-child(${h%gridWidth+1})`)
}),
(!waitAfterSolve || !response.data.some(s => s)) && (await delay(200), clickElement("#recaptcha-verify-button")), await waitForEvent(eventQueue); document.querySelectorAll(".rc-imageselect-dynamic-selected").length > 0;) await delay(1e3)
}
}
if (location.pathname.endsWith("/anchor")) {
settings.createMenu();
registerEventHandler({
name: "auto-open",
condition: () => settings.get("auto_open") && !settings.get("disabled_hosts").includes(HOST),
ready: () => document.contains(document.querySelector(".recaptcha-checkbox")),
start: initializeRecaptcha,
quit: () => {
checkboxObserver.disconnect();
intersectionObserver.disconnect();
isRecaptchaActive = false;
},
running: () => isRecaptchaActive
})
} else {
registerEventHandler({
name: "auto-solve",
condition: () => settings.get("auto_solve") && !settings.get("disabled_hosts").includes(HOST),
ready: () => document.contains(document.querySelector(".rc-imageselect, .rc-imageselect-target")),
start: initializeCaptcha,
quit: () => {
captchaObserver.disconnect();
isCaptchaActive = false;
processEvents(eventQueue)
},
running: () => isCaptchaActive
})
}
async function checkEventHandler(handler) {
if (handler.timedout) return false;
const condition = handler.condition();
if (condition === handler.running()) return false;
if (!condition && handler.running()) {
handler.quit();
return false;
}
if (condition && !handler.running()) {
while (!handler.ready()) await delay(200);
handler.start();
return false;
}
}
function createCanvas(width, height) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
return canvas;
}
function canvasToBase64(canvas) {
return canvas.toDataURL("image/jpeg").replace(/data:image\/[a-z]+;base64,/g, "");
}
function isCanvasEmpty(canvas) {
try {
canvas.getContext("2d", { willReadFrequently: true }).getImageData(0, 0, 1, 1);
} catch {
return true;
}
return false;
}
function delay(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
function initializeCaptcha() {
isCaptchaActive = true;
processEvents(eventQueue);
let captchaTimeout;
captchaObserver = new MutationObserver(() => {
clearTimeout(captchaTimeout);
captchaTimeout = setTimeout(() => processEvents(eventQueue), 200);
});
captchaObserver.observe(document.body, { childList: true, subtree: true });
startCaptchaSolving();
}
function processEvents(queue) {
console.log(queue);
queue.forEach(callback => callback());
queue.splice(0);
}
function registerEventHandler(handler, timeoutDuration) {
handler.timedout = false;
eventHandlers.push(handler);
let timeout;
let interval = setInterval(async () => {
await checkEventHandler(handler) || (clearTimeout(timeout), clearInterval(interval));
}, 400);
timeoutDuration && (timeout = setTimeout(() => clearInterval(interval), timeoutDuration), handler.timedout = true);
}
function waitForEvent(queue) {
return new Promise(resolve => queue.push(resolve));
}
function initializeRecaptcha() {
isRecaptchaActive = true;
checkboxObserver = new MutationObserver(changes => {
if (changes.length === 2) {
handleCheckboxChange();
}
if (changes.length && changes[0].target.classList.contains("recaptcha-checkbox-expired")) {
location.reload();
}
});
checkboxObserver.observe(document.querySelector(".recaptcha-checkbox"), {
attributes: true
});
let isIntersected = false;
intersectionObserver = new IntersectionObserver(() => {
if (!isIntersected) {
isIntersected = true;
handleCheckboxChange();
}
}, {
threshold: 0
});
intersectionObserver.observe(document.body);
}
function isVerifyButtonDisabled() {
return document.querySelector("#recaptcha-verify-button")?.getAttribute("disabled");
}
async function handleCheckboxChange() {
await delay(400);
clickElement(".recaptcha-checkbox");
}
function clickElement(selector) {
document.querySelector(selector)?.click();
}
function getCaptchaHeader() {
return document.querySelector(".rc-doscaptcha-header");
}
function getCaptchaInfo() {
return new Promise(resolve => {
const interval = setInterval(() => {
const instructions = document.querySelector(".rc-imageselect-instructions");
const cells = [...document.querySelectorAll("table tr td")];
const images = cells.map(cell => cell.querySelector("img")); //.filter(c => c).filter(c => c.src.trim());
if (!instructions || cells.concat(images).length < 18) return;
clearInterval(interval);
const lines = instructions.innerText.split("\n");
const task = lines.slice(0, 2).join(" ").replace(/\s+/g, " ").trim();
const type = cells.length === 16 ? 2 : images.some(img => img.classList.contains("rc-image-tile-11")) ? 1 : 0;
const waitAfterSolve = lines.length === 3 && type !== 2;
resolve({ task, type, cells, images, waitAfterSolve });
}, 1000);
});
}