1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
//! High-level SDK convenience helpers.
//!
//! `captchaforge` exposes a layered API:
//!
//! - **Building blocks** (`detect`, `solver`, `stealth`, `warmup`,
//! `cookies`, `behavior`, …), fine-grained primitives a power
//! user composes into a custom flow.
//! - **One-call helpers** (this module), wired-together entry points
//! for the 90% case: prepare the page, navigate, solve, retry,
//! wait for clearance, dismiss CMP banners.
//!
//! The helpers in this module are *thin*. Every one of them
//! delegates to lower-level functions documented in their own
//! modules; they exist so a SDK consumer can write
//! `captchaforge::solve_url(&page, url, None).await?` and not have
//! to remember the right call order.
//!
//! All helpers in this module are re-exported at the crate root
//! ([`crate::prepare_page`], [`crate::solve_url`], etc.), this
//! module is the implementation site, not the canonical import path.
use Context;
use crate::;
/// One-call setup for a fresh page BEFORE the first navigation.
///
/// Call this immediately after creating the page (and BEFORE
/// `page.goto(url)`) so the stealth + profile overrides are
/// installed via `addScriptToEvaluateOnNewDocument` and run on
/// the very first page load, that's the only way to defeat
/// anti-bot fingerprint checks that fire before our solver gets
/// a chance to react.
///
/// `profile = None` (the default) applies **max-coherence** stealth: it scrubs
/// automation tells (`webdriver`, `cdc_*`, phantom, the `chrome` shim, …) while
/// leaving the REAL browser identity. User-Agent, platform, hardwareConcurrency,
/// WebGL, genuine. A real Firefox's own surfaces are self-consistent across
/// JS/HTTP/TLS, so the least-detectable disguise is the real browser with its
/// automation fingerprint removed.
///
/// `profile = Some(persona)` additionally pins that persona's identity. Use it
/// only for an EXPLICIT cross-OS / cross-version persona, and launch via
/// [`guise::launch_profiled_firefox`] so the launch-time prefs make the HTTP
/// User-Agent header agree with the JS identity override (a vanilla launch would
/// leave the request header at the real version while JS reports the persona's
/// a self-inflicted tell).
///
/// Pre-navigation only; run [`crate::warmup::apply_default_warmup`] after `goto`
/// so the interaction signals land on the target page.
pub async
/// One-call solve for an unprepared page navigated to `url`.
///
/// The simplest possible entry point: pass a fresh page and a URL,
/// get back a solve result. Wires:
///
/// 1. [`prepare_page`], stealth + profile BEFORE nav, so the first
/// page load is already de-fingerprinted.
/// 2. `page.goto(url)`: actual navigation.
/// 3. [`crate::warmup::apply_default_warmup`], human-like interaction
/// signals on the loaded target page.
/// 4. [`crate::auto_solve`], detect + solve loop with
/// TOML-discovered chain.
///
/// Returns `Ok(None)` when no captcha was detected after navigation
/// (the typical happy path for sites where stealth alone passed any
/// passive challenge), or `Ok(Some(result))` with the solver outcome.
pub async
/// Dismiss cookie/consent / GDPR / CMP banners that often sit
/// BETWEEN the user and the captcha widget.
///
/// Many anti-bot widgets won't render (or won't accept clicks)
/// while a fixed-position consent overlay covers them. Detection
/// passes, but solving fails because the overlay intercepts every
/// click. Calling this once after `page.goto()` clears the most
/// common banners (OneTrust, Cookiebot, Cookiehub, Quantcast Choice,
/// Didomi, Usercentrics, plain `accept-cookies` IDs) by clicking
/// the obvious "accept all" button when present, falling back to
/// hiding the overlay container when no button matched.
///
/// Returns `Ok(true)` when at least one banner element was clicked
/// or hidden, `Ok(false)` when no candidate matched. Best-effort
/// and idempotent (safe to call unconditionally before any solve).
pub async
/// Inline JS payload for [`dismiss_cookie_consent`].
///
/// Ordered: vendor-specific selectors first (most precise), then
/// aria-label fallbacks, then ID/class textual matchers, then
/// last-resort container-hide. Each candidate must be visible
/// (non-zero bbox + not display:none + not visibility:hidden)
/// hidden buttons are anti-bot honeypots, never auto-click them.
const DISMISS_CMP_JS: &str = r#"(() => {
const candidates = [
'#onetrust-accept-btn-handler',
'#onetrust-pc-btn-handler',
'.ot-pc-refuse-all-handler',
'#CybotCookiebotDialogBodyLevelButtonAcceptAll',
'#CybotCookiebotDialogBodyButtonAccept',
'#cookiehub-button-accept',
'button[mode="primary"][data-testid="uc-accept-all-button"]',
'button[data-testid="uc-deny-all-button"]',
'button.didomi-continue-without-agreeing',
'button#didomi-notice-agree-button',
'button.qc-cmp2-summary-buttons button[mode="primary"]',
'button[aria-label*="accept all" i]',
'button[aria-label*="agree" i]',
'button#cookie-accept',
'button#accept-cookies',
'button.accept-cookies',
'button[id*="accept" i][id*="cookie" i]',
'button[class*="accept" i][class*="cookie" i]',
];
let clicked = 0;
for (const sel of candidates) {
try {
const el = document.querySelector(sel);
if (!el) continue;
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
const cs = window.getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden') continue;
el.click();
clicked++;
break;
} catch (_) { /* keep going */ }
}
if (clicked === 0) {
const overlays = document.querySelectorAll(
'[id*="cookie-consent" i], [id*="cookie-banner" i], ' +
'[class*="cookie-consent" i], [class*="cookie-banner" i], ' +
'#onetrust-banner-sdk, #CybotCookiebotDialog, ' +
'.didomi-popup-container, .qc-cmp2-container'
);
for (const o of overlays) {
try { o.style.display = 'none'; clicked++; } catch (_) {}
}
}
return clicked;
})()"#;
/// Remove third-party chat widgets (Intercom / Drift / HubSpot /
/// Crisp / Tawk.to / Zendesk Chat / LiveChat / Olark / Front /
/// LiveAgent / Userlike) that often render as fixed-position
/// overlays partially covering the captcha widget.
///
/// Same shape as [`dismiss_cookie_consent`]: clicks the close-X
/// button when one is exposed, falls back to hiding the widget
/// container when no button matched. Returns `Ok(true)` when
/// at least one widget was clicked or hidden.
///
/// Best-effort and idempotent, safe to call unconditionally
/// before any solve.
pub async
/// Inline JS payload for [`dismiss_chat_widget`].
///
/// Vendor-specific close-button selectors first, then a
/// last-resort container hide. Mirrors `dismiss_cookie_consent`'s
/// shape so the two helpers are interchangeable from the
/// caller's perspective.
const DISMISS_CHAT_JS: &str = r#"(() => {
const close_buttons = [
'.intercom-launcher-close',
'.intercom-namespace [aria-label*="close" i]',
'button[aria-label="Close"][data-testid*="drift"]',
'#hubspot-messages-iframe-container button[aria-label*="close" i]',
'.crisp-client [aria-label*="close" i]',
'#tawkchat-minify-iconwrapper',
'.zopim [data-test-id*="close"]',
'iframe[title*="LiveChat"] + button',
'.olark-launch-button.olark-state-busy + button',
'#frontApp-close-button',
'button[id*="liveagent"][title*="close" i]',
'#userlike-button-close',
];
let clicked = 0;
for (const sel of close_buttons) {
try {
const el = document.querySelector(sel);
if (!el) continue;
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
el.click();
clicked++;
} catch (_) { /* keep going, clear all that match */ }
}
if (clicked === 0) {
const containers = [
'#intercom-container', '.intercom-namespace',
'#drift-frame-controller', '#drift-frame-chat',
'#hubspot-messages-iframe-container',
'#crisp-client',
'#tawkchat-container',
'#zopim',
'iframe[title*="LiveChat"]',
'.olark-launch-button',
'#frontApp',
'[id^="liveagent_"]',
'#userlike-button',
];
for (const sel of containers) {
try {
document.querySelectorAll(sel).forEach(el => {
el.style.display = 'none';
clicked++;
});
} catch (_) {}
}
}
return clicked;
})()"#;
/// One-call solve with N retries on transient failure.
///
/// Wraps [`crate::auto_solve`] in a fixed retry loop. On each
/// attempt:
///
/// 1. Dismiss any newly-rendered cookie banner.
/// 2. Run the auto_solve chain.
/// 3. If it returns `Ok(Some(r))` with `r.success == true`, return.
/// 4. Otherwise wait `inter_attempt_ms` and try again.
///
/// Use this for high-stakes flows (checkout, account creation)
/// where a single failed attempt should be retried automatically
/// before surfacing a human-fallback to the operator.
///
/// `max_attempts == 0` is treated as 1 (you always get at least
/// one attempt). `inter_attempt_ms` is the wall-clock pause between
/// attempts; 1500-3000ms is a reasonable default for vendors that
/// rate-limit token issuance.
pub async
/// Wait until [`detect::detect`] reports no captcha on the page,
/// with a timeout. Useful between navigation and a protected
/// action: lets the page's passive challenges settle before you
/// try to submit.
///
/// Returns `Ok(true)` when the captcha cleared within the budget,
/// `Ok(false)` on timeout. Polls every 200ms, the cadence is
/// fast enough to feel responsive but slow enough not to spam
/// the eval channel.
pub async