use async_trait::async_trait;
use std::time::Instant;
use crate::antibot::solver::{CaptchaSolver, ChallengePayload, SolveResult, SolverError};
use crate::antibot::ChallengeVendor;
use super::solver::{solve, SolveRequest};
#[derive(Debug, Default)]
pub struct RecaptchaInvisibleAdapter {
proxy_url: Option<String>,
}
impl RecaptchaInvisibleAdapter {
pub fn new() -> Self {
Self::default()
}
pub fn with_proxy(mut self, proxy_url: impl Into<String>) -> Self {
self.proxy_url = Some(proxy_url.into());
self
}
}
#[async_trait]
impl CaptchaSolver for RecaptchaInvisibleAdapter {
fn name(&self) -> &'static str {
"recaptcha-invisible"
}
fn supported_vendors(&self) -> &'static [ChallengeVendor] {
&[ChallengeVendor::Recaptcha]
}
async fn solve(&self, c: ChallengePayload) -> Result<SolveResult, SolverError> {
if !self.supported_vendors().contains(&c.vendor) {
return Err(SolverError::UnsupportedVendor {
adapter: "recaptcha-invisible",
vendor: c.vendor,
});
}
let site_key = c
.sitekey
.as_deref()
.ok_or_else(|| SolverError::Upstream("missing sitekey".to_string()))?;
let action = c.action.as_deref().unwrap_or("submit");
let started = Instant::now();
let req = SolveRequest {
site_key,
site_url: &c.url,
action,
bundle: None,
};
match solve(req, self.proxy_url.as_deref()).await {
Ok(out) => Ok(SolveResult {
token: out.token,
elapsed_ms: out.elapsed_ms,
adapter: "recaptcha-invisible",
}),
Err(e) => {
tracing::debug!(
target: "antibot::recaptcha",
elapsed_ms = started.elapsed().as_millis() as u64,
error = ?e,
"recaptcha invisible solve failed",
);
Err(SolverError::Upstream(e.to_string()))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use url::Url;
fn payload(vendor: ChallengeVendor, sitekey: Option<&str>) -> ChallengePayload {
ChallengePayload {
vendor,
url: Url::parse("https://example.com/login").unwrap(),
sitekey: sitekey.map(String::from),
action: Some("login".into()),
iframe_srcs: vec![],
screenshot_png: None,
}
}
#[test]
fn name_is_stable_identifier() {
let a = RecaptchaInvisibleAdapter::new();
assert_eq!(a.name(), "recaptcha-invisible");
}
#[test]
fn only_vanilla_recaptcha_supported() {
let a = RecaptchaInvisibleAdapter::new();
let v = a.supported_vendors();
assert!(v.contains(&ChallengeVendor::Recaptcha));
assert!(!v.contains(&ChallengeVendor::RecaptchaEnterprise));
assert!(!v.contains(&ChallengeVendor::HCaptcha));
assert!(!v.contains(&ChallengeVendor::CloudflareTurnstile));
}
#[tokio::test]
async fn refuses_unsupported_vendor() {
let a = RecaptchaInvisibleAdapter::new();
let err = a
.solve(payload(ChallengeVendor::HCaptcha, Some("k")))
.await
.unwrap_err();
assert!(matches!(err, SolverError::UnsupportedVendor { .. }));
}
#[tokio::test]
async fn requires_sitekey() {
let a = RecaptchaInvisibleAdapter::new();
let err = a
.solve(payload(ChallengeVendor::Recaptcha, None))
.await
.unwrap_err();
assert!(matches!(err, SolverError::Upstream(_)));
}
#[test]
fn with_proxy_attaches_url() {
let a = RecaptchaInvisibleAdapter::new().with_proxy("http://user:pass@proxy.example:8080");
assert!(a.proxy_url.is_some());
}
}