NopeCHA - Automated reCAPTCHA Solver

AI for Automatic reCAPTCHA Recognition

As of 29.04.2025. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         NopeCHA - Automated reCAPTCHA Solver
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  AI for Automatic reCAPTCHA Recognition
// @author       You
// @require      https://update.greasyfork.org/scripts/534380/1580029/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 API_ENDPOINT = "https://api.nopecha.com";
const GRID_SIZES = { 1: 1, 0: 3, 2: 4 };

UserscriptSettings.define({
    key: {
        name: "Enter your key",
        default: "",
        title: "",
    },
    disabled_hosts: {
        name: "Disabled Hosts",
        default: "",
        title: "",
        formatter: (val) => val.split(",").map(s => s.trim()).filter(Boolean),
    },
    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 settings = UserscriptSettings;
const POLL_TIMEOUT = 60000;
const MAX_ATTEMPTS = 30;

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 = new Headers({
        'Accept': 'application/json',
        'Content-Type': 'application/json',
    })
    if (settings.get("key")) {
        headers.append("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").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");
    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(), c = [...cells];
        type !== 1 && (images = [images[0]]);
        let processedImages = await Promise.all(images.map(s => loadImage(s)));
        if(type === 1) {
            let s = [],
                x = [];
            for(let [index, img] of processedImages.entries()) img.width !== 100 || img.height !== 100 || (s.push(c[index]), x.push(img));
            c = s, processedImages = x
        }
        if(processedImages.length === 0) {
            clickElement("#recaptcha-verify-button");
            await delay(3e3);
            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(c.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)
    }
}

let e = document.referrer;
e = e ? e.split("/")[2] : location.origin

if (location.pathname.endsWith("/anchor")) {
    settings.createMenu();
    registerEventHandler({
        name: "auto-open",
        condition: () => settings.get("auto_open"), //&& !settings.disabled_hosts.includes(e),
        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.disabled_hosts.includes(e),
        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").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) {
    queue.forEach(callback => callback());
    queue.splice(0);
}

function registerEventHandler(handler, timeoutDuration) {
    handler.timedout = false;
    eventHandlers.push(handler);
    let timeout, 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"));
            const waitAfterSolve = lines.length === 3 && type !== 2;
            resolve({ task, type, cells, images, waitAfterSolve });
        }, 1000);
    });
}