mirror of
https://github.com/germondai/trawl.git
synced 2026-08-17 12:11:23 +02:00
refactor(solvers): simplify optional results
This commit is contained in:
@@ -42,7 +42,7 @@ export async function solveGeetestSlide(page: Page, timeoutMs = 30_000): Promise
|
||||
console.log("[geetest] clicking initial verify button")
|
||||
// The div has tabindex and aria-label — use page.mouse.click at its actual coordinates
|
||||
// so the browser dispatches the click to the div (not forced through a covering element).
|
||||
const verifyBox = await verifyBtn.boundingBox().catch(() => null)
|
||||
const verifyBox = await verifyBtn.boundingBox().catch(() => undefined)
|
||||
if (verifyBox) {
|
||||
await page.mouse.click(verifyBox.x + verifyBox.width / 2, verifyBox.y + verifyBox.height / 2)
|
||||
} else {
|
||||
@@ -127,7 +127,7 @@ export async function solveGeetestSlide(page: Page, timeoutMs = 30_000): Promise
|
||||
.locator(DRAG_HANDLE_V3)
|
||||
.first()
|
||||
.boundingBox()
|
||||
.catch(() => null)
|
||||
.catch(() => undefined)
|
||||
if (v3Handle) {
|
||||
sliderBox = v3Handle
|
||||
console.log(
|
||||
|
||||
@@ -59,11 +59,11 @@ async function detectTurnstile(page: Page, timeoutMs: number): Promise<boolean>
|
||||
)
|
||||
)
|
||||
return "iframe"
|
||||
const inp = document.querySelector('input[name="cf-turnstile-response"]') as HTMLInputElement | null
|
||||
if (inp && inp.value.length > 10) return "token"
|
||||
return null
|
||||
const input = document.querySelector('input[name="cf-turnstile-response"]')
|
||||
if (input instanceof HTMLInputElement && input.value.length > 10) return "token"
|
||||
return
|
||||
})
|
||||
.catch(() => null)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (viaDOM === "iframe") return true
|
||||
if (viaDOM === "token") {
|
||||
|
||||
@@ -134,14 +134,14 @@ export async function solveRecaptchaV2(page: Page, timeoutMs = 30_000): Promise<
|
||||
(await bframe
|
||||
.locator(".rc-audiochallenge-tdownload-link")
|
||||
.getAttribute("href", { timeout: 1000 })
|
||||
.catch(() => null)) ||
|
||||
.catch(() => undefined)) ||
|
||||
""
|
||||
|
||||
// Reject blob: URLs — they're browser-internal and can't be fetched from outside
|
||||
const audioHref = rawHref && !rawHref.startsWith("blob:") ? rawHref : null
|
||||
const audioHref = rawHref && !rawHref.startsWith("blob:") ? rawHref : undefined
|
||||
console.log("[recaptcha] raw audio href:", rawHref?.slice(0, 100) ?? "none")
|
||||
|
||||
console.log("[recaptcha] audio URL:", audioHref?.slice(0, 80) ?? "null")
|
||||
console.log("[recaptcha] audio URL:", audioHref?.slice(0, 80) ?? "unavailable")
|
||||
|
||||
if (!audioHref) {
|
||||
console.log("[recaptcha] audio URL not found, retry", attempt)
|
||||
|
||||
@@ -26,17 +26,17 @@ const FFMPEG = process.env.FFMPEG_PATH ?? "ffmpeg"
|
||||
const GOOGLE_STT =
|
||||
"https://www.google.com/speech-api/v2/recognize?output=json&lang=en-US&key=AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw"
|
||||
|
||||
export async function transcribeAudio(audioUrl: string, signal?: AbortSignal): Promise<string | null> {
|
||||
export async function transcribeAudio(audioUrl: string, signal?: AbortSignal): Promise<string | undefined> {
|
||||
return STT_URL ? transcribeWhisper(audioUrl, signal) : transcribeGoogle(audioUrl, signal)
|
||||
}
|
||||
|
||||
async function transcribeWhisper(audioUrl: string, signal?: AbortSignal): Promise<string | null> {
|
||||
async function transcribeWhisper(audioUrl: string, signal?: AbortSignal): Promise<string | undefined> {
|
||||
try {
|
||||
const res = await fetch(audioUrl, {
|
||||
signal,
|
||||
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/149" },
|
||||
})
|
||||
if (!res.ok) return null
|
||||
if (!res.ok) return
|
||||
|
||||
const form = new FormData()
|
||||
form.append("file", await res.blob(), "audio.mp3")
|
||||
@@ -48,16 +48,16 @@ async function transcribeWhisper(audioUrl: string, signal?: AbortSignal): Promis
|
||||
if (STT_KEY) headers.Authorization = `Bearer ${STT_KEY}`
|
||||
|
||||
const sttRes = await fetch(STT_URL, { method: "POST", headers, body: form, signal })
|
||||
if (!sttRes.ok) return null
|
||||
if (!sttRes.ok) return
|
||||
return clean(await sttRes.text())
|
||||
} catch {
|
||||
return null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Converts MP3 → FLAC via ffmpeg, sends to Google's free Speech API.
|
||||
// Tries 8000 Hz first (reCAPTCHA audio is typically low-bitrate), then 16000 Hz.
|
||||
async function transcribeGoogle(audioUrl: string, signal?: AbortSignal): Promise<string | null> {
|
||||
async function transcribeGoogle(audioUrl: string, signal?: AbortSignal): Promise<string | undefined> {
|
||||
const id = randomUUID().slice(0, 8)
|
||||
const mp3 = `/tmp/trawl-${id}.mp3`
|
||||
const flac8 = `/tmp/trawl-${id}-8k.flac`
|
||||
@@ -75,13 +75,13 @@ async function transcribeGoogle(audioUrl: string, signal?: AbortSignal): Promise
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.log("[stt] audio download failed:", res.status)
|
||||
return null
|
||||
return
|
||||
}
|
||||
const audioBytes = await res.arrayBuffer()
|
||||
console.log("[stt] audio downloaded:", audioBytes.byteLength, "bytes, type:", res.headers.get("content-type"))
|
||||
if (audioBytes.byteLength < 1000) {
|
||||
console.log("[stt] audio too small")
|
||||
return null
|
||||
return
|
||||
}
|
||||
await Bun.write(mp3, audioBytes)
|
||||
|
||||
@@ -127,10 +127,10 @@ async function transcribeGoogle(audioUrl: string, signal?: AbortSignal): Promise
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return null
|
||||
return
|
||||
} catch (err) {
|
||||
console.log("[stt] error:", err instanceof Error ? err.message : err)
|
||||
return null
|
||||
return
|
||||
} finally {
|
||||
await $`rm -f ${mp3} ${flac8} ${flac16}`.nothrow().catch(() => {})
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ export async function solveTurnstile(page: Page, timeoutMs = 25_000): Promise<bo
|
||||
// Firefox Fission isolates CF iframe in a separate process — frame DOM is inaccessible.
|
||||
// frame.frameElement() gives us the <iframe> element in the parent page, so we can
|
||||
// get its bounding box and click at the checkbox position via page mouse coordinates.
|
||||
const frameEl = await cfFrame.frameElement().catch(() => null)
|
||||
const box = frameEl ? await frameEl.boundingBox().catch(() => null) : null
|
||||
const frameEl = await cfFrame.frameElement().catch(() => undefined)
|
||||
const box = frameEl ? await frameEl.boundingBox().catch(() => undefined) : undefined
|
||||
|
||||
if (box && box.width > 20) {
|
||||
// Checkbox is in the left portion of the Turnstile iframe (approx x+24, vertically centered).
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function waitForChallengeResolution(
|
||||
): Promise<"ok" | "ip-blocked" | "timeout"> {
|
||||
const deadline = Date.now() + Math.max(timeoutMs, 30_000)
|
||||
let lastClickAttempt = 0
|
||||
let cfClearanceAt: number | null = null
|
||||
let cfClearanceAt: number | undefined
|
||||
|
||||
// Only count cf_clearance for the current domain — warm browser may have cookies from prior domains
|
||||
const targetHost = (() => {
|
||||
@@ -61,7 +61,7 @@ export async function waitForChallengeResolution(
|
||||
targetHost.endsWith(c.domain.replace(/^\./, ""))),
|
||||
)
|
||||
if (hasDomainClearance) {
|
||||
if (cfClearanceAt === null) {
|
||||
if (cfClearanceAt === undefined) {
|
||||
cfClearanceAt = Date.now()
|
||||
console.log("[challenge] cf_clearance obtained")
|
||||
}
|
||||
@@ -103,8 +103,8 @@ async function attemptTurnstileClick(page: Page): Promise<boolean> {
|
||||
if (clicked) return true
|
||||
|
||||
// C: page-coordinate click — bypasses Fission by clicking on page instead of inside frame
|
||||
const frameEl = await frame.frameElement().catch(() => null)
|
||||
const box = frameEl ? await frameEl.boundingBox().catch(() => null) : null
|
||||
const frameEl = await frame.frameElement().catch(() => undefined)
|
||||
const box = frameEl ? await frameEl.boundingBox().catch(() => undefined) : undefined
|
||||
if (box && box.width > 20) {
|
||||
const cx = box.x + Math.min(24, box.width * 0.15)
|
||||
const cy = box.y + box.height / 2
|
||||
@@ -138,11 +138,9 @@ async function clickShadowCheckbox(_page: Page, frame: Frame): Promise<boolean>
|
||||
try {
|
||||
const handle = await frame
|
||||
.evaluateHandle(() => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: shadow DOM traversal
|
||||
const roots: any[] = []
|
||||
// biome-ignore lint/suspicious/noExplicitAny: shadow DOM traversal
|
||||
function collect(node: any) {
|
||||
if (!node) return
|
||||
type ShadowHost = ParentNode & { shadowRootUnl?: ShadowRoot }
|
||||
const roots: ShadowRoot[] = []
|
||||
function collect(node: ShadowHost) {
|
||||
if (node.shadowRootUnl) {
|
||||
roots.push(node.shadowRootUnl)
|
||||
collect(node.shadowRootUnl)
|
||||
@@ -157,20 +155,19 @@ async function clickShadowCheckbox(_page: Page, frame: Frame): Promise<boolean>
|
||||
collect(document)
|
||||
return roots
|
||||
})
|
||||
.catch(() => null)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!handle) return false
|
||||
|
||||
const props = await handle.getProperties().catch(() => null)
|
||||
const props = await handle.getProperties().catch(() => undefined)
|
||||
if (!props) return false
|
||||
|
||||
for (const [, shadowHandle] of props) {
|
||||
const el = shadowHandle.asElement()
|
||||
if (!el) continue
|
||||
const checkboxHandle = await el
|
||||
// biome-ignore lint/suspicious/noExplicitAny: shadow root handle
|
||||
.evaluateHandle((root: any) => root.querySelector('input[type="checkbox"]'))
|
||||
.catch(() => null)
|
||||
.evaluateHandle((root: ParentNode) => root.querySelector('input[type="checkbox"]'))
|
||||
.catch(() => undefined)
|
||||
if (!checkboxHandle) continue
|
||||
const checkbox = checkboxHandle.asElement()
|
||||
if (!checkbox) continue
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function waitForImpervaResolution(
|
||||
originalUrl?: string,
|
||||
): Promise<"ok" | "ip-blocked" | "timeout"> {
|
||||
const deadline = Date.now() + Math.max(timeoutMs, 30_000)
|
||||
let sensorCookieAt: number | null = null
|
||||
let sensorCookieAt: number | undefined
|
||||
|
||||
const targetHost = (() => {
|
||||
try {
|
||||
@@ -51,7 +51,7 @@ export async function waitForImpervaResolution(
|
||||
)
|
||||
|
||||
if (hasSensorCookie) {
|
||||
if (sensorCookieAt === null) {
|
||||
if (sensorCookieAt === undefined) {
|
||||
sensorCookieAt = Date.now()
|
||||
console.log("[imperva] sensor cookie obtained")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user