Spaces:
Paused
Paused
| // reCAPTCHA v2 audio-fallback flow. | |
| // | |
| // Strategy: click checkbox β if image challenge appears, switch to audio | |
| // challenge β download audio β transcribe with Whisper β submit answer. | |
| // | |
| // Returns true on success, false otherwise. Throws nothing. | |
| const audio = require('./audio'); | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| function findFrame(page, predicate) { | |
| return page.frames().find((f) => { | |
| const u = f.url() || ''; | |
| return predicate(u); | |
| }); | |
| } | |
| async function solveRecaptchaV2(page, opts = {}) { | |
| const { timeout = 60000, attempts = 3 } = opts; | |
| const deadline = Date.now() + timeout; | |
| // 1) The checkbox (anchor) iframe β click it to start the challenge | |
| const anchor = findFrame(page, (u) => u.includes('/recaptcha/api2/anchor') || u.includes('/recaptcha/enterprise/anchor')); | |
| if (!anchor) return false; | |
| try { | |
| const cb = anchor.locator('#recaptcha-anchor'); | |
| await cb.click({ timeout: 5000 }); | |
| } catch { | |
| return false; | |
| } | |
| await sleep(1500); | |
| // 2) Wait for the bframe (challenge popup); if no popup, checkbox alone passed. | |
| let bframe = null; | |
| for (let i = 0; i < 10 && !bframe; i++) { | |
| bframe = findFrame(page, (u) => u.includes('/recaptcha/api2/bframe') || u.includes('/recaptcha/enterprise/bframe')); | |
| if (!bframe) await sleep(500); | |
| } | |
| if (!bframe) { | |
| // Token might already be set | |
| const ok = await page.evaluate(() => { | |
| const t = document.getElementById('g-recaptcha-response'); | |
| return !!(t && t.value); | |
| }); | |
| return ok; | |
| } | |
| // 3) Switch to audio challenge | |
| try { | |
| await bframe.locator('#recaptcha-audio-button').click({ timeout: 5000 }); | |
| } catch { | |
| return false; | |
| } | |
| await sleep(1500); | |
| // Detect "automated requests" lockout | |
| const locked = await bframe | |
| .locator('text=/automated queries|try again later/i') | |
| .first() | |
| .isVisible({ timeout: 1000 }) | |
| .catch(() => false); | |
| if (locked) return false; | |
| for (let i = 0; i < attempts && Date.now() < deadline; i++) { | |
| // 4) Get audio source URL | |
| const src = await bframe | |
| .locator('audio#audio-source, .rc-audiochallenge-tdownload-link') | |
| .first() | |
| .getAttribute('href') | |
| .catch(() => null) || | |
| await bframe.locator('audio#audio-source').first().getAttribute('src').catch(() => null); | |
| if (!src) return false; | |
| // 5) Transcribe via local Whisper | |
| let answer = ''; | |
| try { | |
| answer = (await audio.transcribeUrl(page, src)).trim().toLowerCase(); | |
| } catch { | |
| return false; | |
| } | |
| if (!answer) continue; | |
| // 6) Type the answer + verify | |
| try { | |
| const input = bframe.locator('#audio-response'); | |
| await input.fill(''); | |
| await input.type(answer, { delay: 60 + Math.random() * 60 }); | |
| await bframe.locator('#recaptcha-verify-button').click(); | |
| } catch { | |
| return false; | |
| } | |
| await sleep(2500); | |
| // 7) Check if solved | |
| const solved = await page.evaluate(() => { | |
| const t = document.getElementById('g-recaptcha-response'); | |
| return !!(t && t.value); | |
| }); | |
| if (solved) return true; | |
| // If wrong, reCAPTCHA may show "Multiple solutions required" or refresh β loop. | |
| const errVisible = await bframe | |
| .locator('text=/incorrect|try again/i') | |
| .first() | |
| .isVisible({ timeout: 500 }) | |
| .catch(() => false); | |
| if (errVisible) { | |
| try { await bframe.locator('#recaptcha-reload-button').click({ timeout: 1500 }); } catch {} | |
| await sleep(1500); | |
| } | |
| } | |
| return false; | |
| } | |
| module.exports = { solveRecaptchaV2 }; | |