Solve captchas through the 2captcha API, not its extension
The two Tradeify automations clicked the 2captcha *extension's* solve button and waited for it to go green. That needed a second extension installed and signed in, put a real mouse click on a button whose markup is 2captcha's to change, and gave a run no way to tell "still solving" from "not installed". Replaced with one `solveCaptcha` step. No mouse: the extension reads the widget's sitekey out of the page, the dashboard buys a token from 2captcha, and the extension puts it in the response field and fires the page's own callback. captcha-hook.js is the part that earns its keep. A widget rendered programmatically puts nothing in the DOM — no sitekey, and no callback. Most pages only enable submit from that callback, so filling the hidden field alone leaves the page looking unsolved. The hook wraps turnstile/grecaptcha/hcaptcha `render` at document_start to hold on to what the page passed. Solves go through 2captcha's residential proxy. Its API rejects host names in proxyAddress, so na.proxy.2captcha.com is resolved to an IP first (cached); the gateway was confirmed to work addressed that way. Covers reCAPTCHA v2 (incl. invisible and Enterprise), hCaptcha and Turnstile. NOT reCAPTCHA v3: a v3 page carries an anchor iframe with k=, so it is detected as v2 and would be solved with the wrong task type. Everything else unsupported fails loudly instead. The API key and proxy credentials are hardcoded in the captcha route on request. Unlike the sqlite database, that file is in the repo, so they travel with it. TWOCAPTCHA_API_KEY / TWOCAPTCHA_PROXY and the settings rows still override; neither has a field on the Settings page. Runner VERSION and RUNNER_EXPECTED_VERSION both to 0.17.0 — they are a pair, and a mismatch shows as a red "restart your runner" banner that blames the wrong process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
25289cb9b3
commit
159389be35
+32
-13
@@ -39,6 +39,28 @@ export type AutomationStep =
|
||||
settleMs?: number;
|
||||
label?: string;
|
||||
}
|
||||
// Solve the page's captcha through the 2captcha API.
|
||||
//
|
||||
// Replaces the old pair of steps that clicked the 2captcha *extension's* own
|
||||
// "solve" button and waited for it to go green. That needed a second
|
||||
// extension installed and signed in on the machine, put a real mouse click on
|
||||
// a button whose position and markup are 2captcha's to change, and gave the
|
||||
// run no way to tell "still solving" from "the extension is not there".
|
||||
//
|
||||
// This reads the widget's parameters out of the page, has the dashboard ask
|
||||
// 2captcha for a token, and hands the token back to the page's own callback.
|
||||
// No mouse involved.
|
||||
//
|
||||
// `provider` is normally left off: the widget is detected. Set it when a page
|
||||
// carries more than one kind. `selector` likewise scopes detection to a
|
||||
// container when several widgets are on the page at once.
|
||||
| {
|
||||
action: 'solveCaptcha';
|
||||
provider?: 'turnstile' | 'recaptcha' | 'hcaptcha';
|
||||
selector?: string;
|
||||
timeoutSeconds?: number;
|
||||
label?: string;
|
||||
}
|
||||
// Run `steps` several times over. `times` fixes the count here; `timesFrom`
|
||||
// takes it from an input the user fills in on the dashboard. The block is
|
||||
// unrolled before the runner ever sees it — see resolveSteps.
|
||||
@@ -142,16 +164,9 @@ export const FIRMS: Firm[] = [
|
||||
// every site build, so they are not safe to select on.
|
||||
|
||||
{ action: 'click', selector: 'a.add_account_btn', label: 'Open Add Account' },
|
||||
{ action: 'waitFor', selector: 'div.account_types:nth-child(3) > div[role="radiogroup"] > div > div:nth-child(2)', timeoutSeconds: 30, label: 'Wait for form' },
|
||||
{ action: 'click', selector: 'div.account_types:nth-child(3) > div[role="radiogroup"] > div > div:nth-child(2)'},
|
||||
{ action: 'click', selector: 'div.account_types:nth-child(7) span:last-child'},
|
||||
{ action: 'click', selector: 'div.summary_section div.MuiTextField-root input'},
|
||||
{ action: 'type', selector: 'div.summary_section div.MuiTextField-root input', text: 'MX8'},
|
||||
{ action: 'click', selector: 'div.summary_section div.MuiTextField-root + button'},
|
||||
{ action: 'click', selector: 'div.captcha-solver[data-state="ready"]'},
|
||||
{ action: 'waitFor', selector: "div.captcha-solver[data-state='solved']", timeoutSeconds: 120,
|
||||
label: 'Wait for the challenge' },
|
||||
{ action: 'click', selector: 'div.summary_section > button:last-child', label: 'Open Add Account' },
|
||||
|
||||
{ action: 'solveCaptcha', timeoutSeconds: 180, label: 'Solve the challenge' },
|
||||
|
||||
// TODO: the rest of the purchase flow. Confirm each selector with
|
||||
// `clicker.py locate` before adding it here.
|
||||
|
||||
@@ -172,9 +187,7 @@ export const FIRMS: Firm[] = [
|
||||
{ action: 'repeat', timesFrom: 'count', steps: [
|
||||
// one purchase — the steps you already have
|
||||
{ action: 'click', selector: '.status-failed button.reset_btn', label: 'Reset Account' },
|
||||
{ action: 'waitFor', selector: "div.captcha-solver[data-state='ready']", timeoutSeconds: 30, label: 'Wait for the solver' },
|
||||
{ action: 'click', selector: 'div.captcha-solver[data-state="ready"]'},
|
||||
{ action: 'waitFor', selector: "div.captcha-solver[data-state='solved']", timeoutSeconds: 120, label: 'Wait for the challenge' },
|
||||
{ action: 'solveCaptcha', timeoutSeconds: 180, label: 'Solve the challenge' },
|
||||
{ action: 'click', selector: 'button.cancelBtn + button.modalActionBtn' },
|
||||
|
||||
{ action: 'waitFor', selector: '.reset_acc_modal', absent: true, timeoutSeconds: 30, label: 'Wait for the modal to close' },
|
||||
@@ -286,6 +299,11 @@ function resolveOne(firm: Firm, step: AutomationStep): ResolvedStep {
|
||||
// Acts on nothing, so no signed-out guard — same treatment as waitFor.
|
||||
return { ...step, urlPattern: firm.urlPattern, openUrl: firm.url };
|
||||
}
|
||||
if (step.action === 'solveCaptcha') {
|
||||
// No guard either, and deliberately: a login form is exactly where a
|
||||
// captcha shows up, so auth steps have to be able to solve one.
|
||||
return { ...step, urlPattern: firm.urlPattern, openUrl: firm.url };
|
||||
}
|
||||
if (step.action === 'repeat') {
|
||||
// expand() peels these off first; reaching here means a caller bypassed it.
|
||||
throw new Error('repeat steps must be expanded, not resolved directly');
|
||||
@@ -367,6 +385,7 @@ export function describeStep(step: AutomationStep): string {
|
||||
? `wait for ${step.selector} to disappear`
|
||||
: `wait for ${step.selector}`;
|
||||
case 'scrollToLoad': return `scroll to load all ${step.selector}`;
|
||||
case 'solveCaptcha': return `solve the ${step.provider ?? 'page'} captcha with 2captcha`;
|
||||
case 'repeat': {
|
||||
const inner = step.steps.length;
|
||||
const count = step.timesFrom ? `{${step.timesFrom}}` : `${step.times ?? 1}`;
|
||||
|
||||
@@ -185,6 +185,13 @@ seedSetting.run('tick_interval_seconds', '60');
|
||||
seedSetting.run('master_dashboard_url', 'https://master.juicerroom.com');
|
||||
seedSetting.run('instance_name', '');
|
||||
seedSetting.run('trading_hours', 'full_cme');
|
||||
// Solves the captchas the AutoBuyer runs into. Blank uses the key hardcoded in
|
||||
// app/api/autobuyer/captcha; set this (or TWOCAPTCHA_API_KEY) to bill another
|
||||
// account.
|
||||
seedSetting.run('twocaptcha_api_key', '');
|
||||
// Blank falls back to the proxy hardcoded in app/api/autobuyer/captcha; `none`
|
||||
// solves without one.
|
||||
seedSetting.run('twocaptcha_proxy', '');
|
||||
// Mirrors the in-memory scheduler state so a restart can resume it.
|
||||
seedSetting.run('scheduler_running', '0');
|
||||
seedSetting.run('scheduler_action', 'Buy');
|
||||
@@ -620,7 +627,7 @@ export const RUNNER_TIMEOUT_MS = 7000;
|
||||
/** The runner version this server's step vocabulary requires. A running process
|
||||
* doesn't reload when the source changes, so an older one silently fails on
|
||||
* steps it predates — the dashboard warns instead. */
|
||||
export const RUNNER_EXPECTED_VERSION = '0.16.0';
|
||||
export const RUNNER_EXPECTED_VERSION = '0.17.0';
|
||||
|
||||
export interface RunnerHeartbeat {
|
||||
at: number;
|
||||
|
||||
Reference in New Issue
Block a user