// ==UserScript== // @name Recaptcha Solver // @namespace https://github.com/marmoris-x/tampermonkey-scripts // @version 3.0.5 // @author marmoris-x // @description Automatically solves reCAPTCHA v2 audio challenges via speech recognition // @license MIT // @icon https://upload.wikimedia.org/wikipedia/commons/thumb/a/ad/RecaptchaLogo.svg/1280px-RecaptchaLogo.svg.png // @supportURL https://github.com/marmoris-x/tampermonkey-scripts/issues // @downloadURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Recaptcha%20Solver.user.js // @updateURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Recaptcha%20Solver.user.js // @match https://www.google.com/recaptcha/* // @match https://google.com/recaptcha/* // @match https://www.recaptcha.net/recaptcha/* // @match https://recaptcha.net/recaptcha/* // @match https://www.google.com/sorry/index* // @sandbox raw // @connect engageub.pythonanywhere.com // @connect engageub1.pythonanywhere.com // @grant GM_addElement // @grant GM_xmlhttpRequest // @run-at document-idle // ==/UserScript== (function () { 'use strict'; function createLogger(prefix, debugMode = false) { const tag = `[${prefix}]`; return { log(...args) { console.log(tag, ...args); }, warn(...args) { console.warn(tag, ...args); }, error(...args) { console.error(tag, ...args); }, info(...args) { console.info(tag, ...args); }, debug(...args) { if (debugMode) console.debug(tag, ...args); } }; } function waitForElement(selector, timeout = 1e4, root = document.body) { return new Promise((resolve, reject) => { const existing = root.querySelector(selector); if (existing) return resolve(existing); let timer = null; const observer = new MutationObserver((mutations) => { var _a, _b; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; if ((_a = node.matches) == null ? void 0 : _a.call(node, selector)) { cleanup(); return resolve(node); } const child = (_b = node.querySelector) == null ? void 0 : _b.call(node, selector); if (child) { cleanup(); return resolve(child); } } } }); const cleanup = () => { observer.disconnect(); if (timer !== null) clearTimeout(timer); }; observer.observe(root, { childList: true, subtree: true }); if (timeout > 0) { timer = setTimeout(() => { cleanup(); reject(new Error(`waitForElement timeout: ${selector}`)); }, timeout); } }); } function observeMutations(callback, root = document.body) { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { callback(node, observer); } } } }); observer.observe(root, { childList: true, subtree: true }); return observer; } function isVisible(el) { if (!el || el.offsetParent === null) return false; const style = window.getComputedStyle(el); return style.display !== "none" && style.visibility !== "hidden"; } function qs(selector, root = document) { return root.querySelector(selector); } function fetchJSON(url, opts = {}) { const retries = opts.retries || 0; const timeout = opts.timeout || 15e3; return new Promise((resolve, reject) => { function attempt(n) { GM_xmlhttpRequest({ method: "GET", url, timeout, anonymous: opts.anonymous !== false, onload(r) { if (r.status >= 200 && r.status < 300) { try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(new Error("JSON parse failed: " + e.message)); } } else if (n < retries) { attempt(n + 1); } else { reject(new Error("HTTP " + r.status + " for " + url)); } }, onerror() { if (n < retries) attempt(n + 1); else reject(new Error("Network error for " + url)); }, ontimeout() { if (n < retries) attempt(n + 1); else reject(new Error("Timeout for " + url)); } }); } attempt(0); }); } let latencies = []; function measureServerLatencies(servers) { const log2 = createLogger("Recaptcha Solver"); latencies = servers.map(() => Infinity); return Promise.all(servers.map((url, i) => { const t0 = Date.now(); return fetchJSON(url, { timeout: 8e3 }).then(() => { latencies[i] = Date.now() - t0; log2.log(`Ping ${url}: ${latencies[i]}ms`); }).catch(() => { latencies[i] = Infinity; }); })); } function getBestServer(servers, latenciesArr, exclude) { let best = null; let bestMs = Infinity; for (let i = 0; i < servers.length; i++) { if (servers[i] === exclude) continue; if (latenciesArr[i] < bestMs) { bestMs = latenciesArr[i]; best = servers[i]; } } return best || servers.filter((s) => s !== exclude)[0] || servers[0]; } const LANG_MAP = { af: "af-ZA", am: "am-ET", ar: "ar-SA", az: "az-AZ", be: "be-BY", bg: "bg-BG", bn: "bn-BD", bs: "bs-BA", ca: "ca-ES", cs: "cs-CZ", cy: "cy-GB", da: "da-DK", de: "de-DE", el: "el-GR", es: "es-ES", et: "et-EE", eu: "eu-ES", fa: "fa-IR", fi: "fi-FI", fr: "fr-FR", ga: "ga-IE", gl: "gl-ES", gu: "gu-IN", he: "he-IL", hi: "hi-IN", hr: "hr-HR", hu: "hu-HU", hy: "hy-AM", id: "id-ID", is: "is-IS", it: "it-IT", ja: "ja-JP", ka: "ka-GE", kk: "kk-KZ", km: "km-KH", kn: "kn-IN", ko: "ko-KR", lt: "lt-LT", lv: "lv-LV", mk: "mk-MK", ml: "ml-IN", mn: "mn-MN", mr: "mr-IN", ms: "ms-MY", my: "my-MM", nb: "nb-NO", ne: "ne-NP", nl: "nl-NL", pa: "pa-IN", pl: "pl-PL", pt: "pt-BR", ro: "ro-RO", ru: "ru-RU", si: "si-SK", sk: "sk-SK", sl: "sl-SI", sq: "sq-AL", sr: "sr-RS", sv: "sv-SE", sw: "sw-KE", ta: "ta-IN", te: "te-IN", th: "th-TH", tl: "tl-PH", tr: "tr-TR", uk: "uk-UA", ur: "ur-PK", uz: "uz-UZ", vi: "vi-VN", zh: "zh-CN", zu: "zu-ZA" }; function getLang() { var _a; const raw = ((_a = document.querySelector("html")) == null ? void 0 : _a.getAttribute("lang")) || navigator.language || "en-US"; return LANG_MAP[raw] || raw; } const SEL = { AUDIO_BUTTON: "#recaptcha-audio-button", AUDIO_SOURCE: "#audio-source", IMAGE_SELECT: "#rc-imageselect", RESPONSE_FIELD: ".rc-audiochallenge-response-field", AUDIO_ERROR: ".rc-audiochallenge-error-message", AUDIO_RESPONSE: "#audio-response", RELOAD_BUTTON: "#recaptcha-reload-button", STATUS: "#recaptcha-accessible-status", DOSCAPTCHA: ".rc-doscaptcha-body", VERIFY_BUTTON: "#recaptcha-verify-button", RC_BUTTONS: ".rc-buttons", HELP_HOLDER: ".help-button-holder" }; const MAX_ATTEMPTS = 5; const INTERVAL_MS = 1e3; const SUBMIT_GRACE_MS = 3500; const STUCK_TIMEOUT_MS = 45e3; const MAX_RESPONSE_LEN = 100; const AUDIO_BTN_DEBOUNCE_MS = 4e3; const SERVERS = [ "https://engageub.pythonanywhere.com", "https://engageub1.pythonanywhere.com" ]; const BUTTON_ID = "rs-solve-btn"; const HOST_ID = "rs-solve-host"; const HELP_SEL = ".help-button-holder"; const log$1 = createLogger("Recaptcha Solver"); function finish(result, onStateChange, interval) { if (interval) clearInterval(interval); onStateChange(result); } function getTextFromAudio(srcUrl, solverState, excludeServer) { const normalizedUrl = srcUrl.replace(/recaptcha\.net/g, "google.com"); const lang = getLang(); const server = getBestServer(SERVERS, latencies, excludeServer); if (!excludeServer) solverState.requestCount++; solverState.waitingStart = Date.now(); log$1.log(`Request to ${server} | lang:${lang} | attempt:${solverState.requestCount}${excludeServer ? " [retry]" : ""}`); GM_xmlhttpRequest({ method: "POST", url: server, headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: "input=" + encodeURIComponent(normalizedUrl) + "&lang=" + encodeURIComponent(lang), timeout: 3e4, onload(response) { try { const text = (response.responseText || "").trim(); log$1.log('Response: "' + text.substring(0, 80) + '"'); const invalid = !text || text === "0" || text.length < 2 || text.length > MAX_RESPONSE_LEN || /<[a-z][\s\S]*?>/i.test(text); if (invalid) { log$1.log("Invalid response — will reload on next tick"); solverState.waiting = false; return; } const audioBtn = qs(SEL.AUDIO_BUTTON); const audioSrcEl = qs(SEL.AUDIO_SOURCE); const audioRespEl = qs(SEL.AUDIO_RESPONSE); const verifyBtn = qs(SEL.VERIFY_BUTTON); const inAudioMode = audioBtn && window.getComputedStyle(audioBtn).display === "none"; if (inAudioMode && audioSrcEl && audioSrcEl.src === srcUrl && audioRespEl && !audioRespEl.value && verifyBtn) { audioRespEl.value = text; audioRespEl.dispatchEvent(new Event("input", { bubbles: true })); audioRespEl.dispatchEvent(new Event("change", { bubbles: true })); verifyBtn.click(); solverState.submittedAt = Date.now(); log$1.log('Submitted: "' + text + '"'); } else { log$1.log("Page state changed — will retry on next challenge"); } } catch (err) { log$1.error("Response handler error: " + err.message); } finally { solverState.waiting = false; } }, onerror() { log$1.warn("Network error from " + server); if (!excludeServer) { getTextFromAudio(srcUrl, solverState, server); } else { log$1.warn("Both servers failed — releasing lock"); solverState.waiting = false; } }, ontimeout() { log$1.warn("Timeout from " + server); if (!excludeServer) { getTextFromAudio(srcUrl, solverState, server); } else { log$1.warn("Both servers failed — releasing lock"); solverState.waiting = false; } } }); } function startSolver({ state, onStateChange }) { onStateChange("working"); const interval = setInterval(function() { try { const st = state; if (!st) return; const dosEl = qs(SEL.DOSCAPTCHA); if (dosEl && dosEl.innerText.length > 0) { log$1.warn("DoS protection triggered — stopping"); finish("dos", onStateChange, interval); return; } if (st.solved || st.stopped) return; const statusEl = qs(SEL.STATUS); if (statusEl && statusEl.innerText !== st.initialStatus) { log$1.log("Solved"); st.solved = true; finish("success", onStateChange, interval); return; } if (st.requestCount >= MAX_ATTEMPTS) { log$1.warn(`Max attempts (${MAX_ATTEMPTS}) reached`); st.stopped = true; finish("failed", onStateChange, interval); return; } if (st.waiting && Date.now() - st.waitingStart > STUCK_TIMEOUT_MS) { log$1.warn("XHR appears stuck — releasing lock"); st.waiting = false; } const now = Date.now(); const audioBtn = qs(SEL.AUDIO_BUTTON); const imageSelect = qs(SEL.IMAGE_SELECT); if (audioBtn && isVisible(audioBtn) && imageSelect && isVisible(imageSelect) && now - st.audioBtnClickAt > AUDIO_BTN_DEBOUNCE_MS) { log$1.log("Switching to audio challenge"); audioBtn.click(); st.audioBtnClickAt = now; return; } const audioSrcEl = qs(SEL.AUDIO_SOURCE); const reloadBtn = qs(SEL.RELOAD_BUTTON); const audioErrEl = qs(SEL.AUDIO_ERROR); const inGrace = st.submittedAt > 0 && now - st.submittedAt < SUBMIT_GRACE_MS; const isStale = !st.waiting && !inGrace && audioSrcEl && audioSrcEl.src && st.audioUrl === audioSrcEl.src && reloadBtn; const hasError = audioErrEl && audioErrEl.innerText.length > 0 && reloadBtn && !reloadBtn.disabled; if (isStale || hasError) { log$1.log(hasError ? "Error detected — reloading" : "Stale audio — reloading"); reloadBtn.click(); return; } const responseField = qs(SEL.RESPONSE_FIELD); const audioRespEl = qs(SEL.AUDIO_RESPONSE); if (!st.waiting && responseField && isVisible(responseField) && audioRespEl && !audioRespEl.value && audioSrcEl && audioSrcEl.src && audioSrcEl.src.length > 0 && st.audioUrl !== audioSrcEl.src) { st.audioUrl = audioSrcEl.src; st.waiting = true; getTextFromAudio(st.audioUrl, st); } } catch (err) { log$1.error("Interval error: " + err.message); finish("failed", onStateChange, interval); } }, INTERVAL_MS); } function createFreshState(initialStatus = "") { return { stopped: false, solved: false, waiting: false, waitingStart: 0, audioUrl: "", requestCount: 0, submittedAt: 0, audioBtnClickAt: 0, initialStatus }; } const SVG = { bolt: '', spin: '', check: '', retry: '', warn: '' }; const BTN_STATES = { ready: [SVG.bolt, "Solve automatically", "", false], working: [SVG.spin, "Solving...", "rs-working", true], success: [SVG.check, "Solved!", "rs-success", true], failed: [SVG.retry, "Failed — click to retry", "rs-failed", false], dos: [SVG.warn, "Automated query limit reached", "rs-dos", true] }; let _styleSheet = null; function createStyleSheet() { if (_styleSheet) return _styleSheet; _styleSheet = new CSSStyleSheet(); _styleSheet.replaceSync(` :host { display: inline-flex !important; align-items: center !important; justify-content: center !important; vertical-align: middle !important; } .rs-btn-holder { display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; } .rs-btn { background-image: none; background-color: #1a73e8; color: #fff; display: inline-flex; align-items: center; justify-content: center; padding: 0; margin: 0; border: none; border-radius: 4px; cursor: pointer; line-height: 0; vertical-align: middle; transition: background-color 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.25); outline: none; user-select: none; } .rs-btn:not(:disabled):hover { background-color: #1558b0; box-shadow: 0 2px 6px rgba(0,0,0,0.3); transform: translateY(-1px); } .rs-btn:not(:disabled):active { transform: translateY(0) scale(0.96); box-shadow: 0 1px 2px rgba(0,0,0,0.2); } .rs-btn:disabled { cursor: default; opacity: 0.80; } .rs-btn.rs-working { background-color: #f29900; } .rs-btn.rs-success { background-color: #1e8e3e; } .rs-btn.rs-failed { background-color: #d93025; } .rs-btn.rs-failed:not(:disabled):hover { background-color: #b71c1c; } .rs-btn.rs-dos { background-color: #e37400; } .rs-spin { animation: rs-rotate 0.9s linear infinite; transform-origin: center; } @keyframes rs-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .rs-btn:focus-visible { outline: 2px solid #1a73e8; outline-offset: 2px; } `); return _styleSheet; } const log = createLogger("Recaptcha Solver"); function setButtonState(btn, stateName) { if (!btn) return; const s = BTN_STATES[stateName] || BTN_STATES.ready; btn.innerHTML = s[0]; btn.title = s[1]; btn.disabled = s[3]; btn.className = "rc-button goog-inline-block rs-btn" + (s[2] ? " " + s[2] : ""); } function injectButton({ onClick }) { const helpHolder = qs(HELP_SEL); if (!helpHolder) return null; if (document.getElementById(HOST_ID)) return null; const host = document.createElement("div"); host.id = HOST_ID; const shadow = host.attachShadow({ mode: "closed" }); shadow.adoptedStyleSheets = [createStyleSheet()]; const btn = document.createElement("button"); btn.id = BUTTON_ID; btn.tabIndex = 0; setButtonState(btn, "ready"); btn.addEventListener("click", onClick); const wrapper = document.createElement("div"); wrapper.className = "rs-btn-holder"; wrapper.appendChild(btn); shadow.appendChild(wrapper); helpHolder.insertAdjacentElement("afterend", host); log.log("Solve button injected"); return { host, button: btn, shadowRoot: shadow }; } function registerBoot() { const log2 = createLogger("Recaptcha Solver"); measureServerLatencies(SERVERS); let currentState = null; let currentInterval = null; function handleClick(btn) { if (currentState && currentState.stopped && !currentState.solved) { log2.log("Retrying solver..."); const statusEl2 = qs(SEL.STATUS); currentState = createFreshState(statusEl2 ? statusEl2.innerText : ""); currentInterval = startSolver({ state: currentState, onStateChange: (stateName) => { setButtonState(btn, stateName); if (stateName !== "working") currentInterval = null; } }); return; } if (currentInterval || currentState && currentState.solved) return; log2.log("Solver started by user"); const statusEl = qs(SEL.STATUS); currentState = createFreshState(statusEl ? statusEl.innerText : ""); currentInterval = startSolver({ state: currentState, onStateChange: (stateName) => { setButtonState(btn, stateName); if (stateName !== "working") currentInterval = null; } }); } createStyleSheet(); waitForElement(HELP_SEL, 0).then(() => { const btnData = injectButton({ onClick() { handleClick(this); } }); if (btnData) { log2.log("Boot complete"); } }); observeMutations(() => { if (!document.getElementById(HOST_ID) && qs(HELP_SEL)) { log2.log("Button lost — re-injecting"); injectButton({ onClick() { handleClick(this); } }); } }); } if (window.location.href.includes("bframe")) { registerBoot(); } })();