use crate::{detect, solver, stealth, stealth_profiles, warmup, StealthProfile};
pub async fn prepare_page(
page: &chromiumoxide::Page,
profile: Option<StealthProfile>,
) -> anyhow::Result<()> {
if let Err(e) = stealth::apply_stealth(page).await {
tracing::debug!("prepare_page: apply_stealth failed (continuing): {e}");
}
if let Some(p) = profile {
if let Err(e) = stealth_profiles::apply_stealth_profile(page, &p).await {
tracing::debug!("prepare_page: apply_stealth_profile failed (continuing): {e}");
}
}
if let Err(e) = warmup::apply_default_warmup(page).await {
tracing::debug!("prepare_page: warmup failed (continuing): {e}");
}
Ok(())
}
pub async fn solve_url(
page: &chromiumoxide::Page,
url: &str,
profile: Option<StealthProfile>,
) -> anyhow::Result<Option<solver::CaptchaSolveResult>> {
prepare_page(page, profile).await?;
page.goto(url)
.await
.map_err(|e| anyhow::anyhow!("solve_url: goto({url}) failed: {e}"))?;
crate::auto_solve(page).await
}
pub async fn dismiss_cookie_consent(page: &chromiumoxide::Page) -> anyhow::Result<bool> {
let dismissed = page
.evaluate(DISMISS_CMP_JS)
.await
.ok()
.and_then(|r| r.into_value::<i64>().ok())
.unwrap_or(0);
Ok(dismissed > 0)
}
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;
})()"#;
pub async fn dismiss_chat_widget(page: &chromiumoxide::Page) -> anyhow::Result<bool> {
let dismissed = page
.evaluate(DISMISS_CHAT_JS)
.await
.ok()
.and_then(|r| r.into_value::<i64>().ok())
.unwrap_or(0);
Ok(dismissed > 0)
}
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;
})()"#;
pub async fn auto_solve_with_retries(
page: &chromiumoxide::Page,
max_attempts: u32,
inter_attempt_ms: u64,
) -> anyhow::Result<Option<solver::CaptchaSolveResult>> {
let attempts = max_attempts.max(1);
let mut last: Option<solver::CaptchaSolveResult> = None;
for attempt in 0..attempts {
let _ = dismiss_cookie_consent(page).await;
match crate::auto_solve(page).await? {
None => return Ok(None),
Some(r) if r.success => return Ok(Some(r)),
Some(r) => {
last = Some(r);
if attempt + 1 < attempts {
tokio::time::sleep(std::time::Duration::from_millis(inter_attempt_ms))
.await;
}
}
}
}
Ok(last)
}
pub fn recommended_chromium_args() -> Vec<&'static str> {
vec![
"--disable-blink-features=AutomationControlled",
"--exclude-switches=enable-automation",
"--disable-features=TranslateUI,AutomationControlled",
"--disable-dev-shm-usage",
"--use-angle=swiftshader-webgl",
"--noerrdialogs",
]
}
pub async fn wait_for_no_captcha(
page: &chromiumoxide::Page,
timeout_ms: u64,
) -> anyhow::Result<bool> {
let deadline =
std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
while std::time::Instant::now() < deadline {
let info = detect::detect(page).await?;
if !detect::is_captcha(&info) {
return Ok(true);
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
Ok(false)
}
#[cfg(test)]
mod chromium_args_tests {
use super::*;
#[test]
fn recommended_args_include_anti_automation_lockdown() {
let args = recommended_chromium_args();
let blob = args.join(" ");
assert!(
blob.contains("AutomationControlled"),
"must drop the navigator.webdriver tell"
);
assert!(
blob.contains("enable-automation"),
"must exclude the chromedriver enable-automation switch"
);
}
#[test]
fn recommended_args_include_dev_shm_workaround() {
let args = recommended_chromium_args();
assert!(args.iter().any(|a| a.contains("disable-dev-shm-usage")));
}
#[test]
fn recommended_args_pin_deterministic_webgl_backend() {
let args = recommended_chromium_args();
assert!(args.iter().any(|a| a.contains("use-angle")));
}
#[test]
fn recommended_args_are_static_str_so_caller_can_borrow() {
let args = recommended_chromium_args();
let _: &[&'static str] = args.as_slice();
}
}