use super::*;
use crate::captcha_detect::DetectedCaptcha;
use std::sync::Arc;
use std::time::Instant;
pub struct RacingSolver {
name: &'static str,
inner: Vec<Arc<dyn CaptchaSolver>>,
}
impl RacingSolver {
pub fn new(name: &'static str) -> Self {
Self {
name,
inner: Vec::new(),
}
}
pub fn with<S: CaptchaSolver + 'static>(mut self, s: S) -> Self {
self.inner.push(Arc::new(s));
self
}
pub fn add<S: CaptchaSolver + 'static>(&mut self, s: S) {
self.inner.push(Arc::new(s));
}
pub fn inner(&self) -> &[Arc<dyn CaptchaSolver>] {
&self.inner
}
}
#[async_trait]
impl CaptchaSolver for RacingSolver {
fn name(&self) -> &'static str {
self.name
}
fn method(&self) -> SolveMethod {
self.inner
.first()
.map(|s| s.method())
.unwrap_or(SolveMethod::CrowdSourced)
}
fn supports(&self, kind: &DetectedCaptcha) -> bool {
self.inner.iter().any(|s| s.supports(kind))
}
async fn solve(&self, page: &Page, info: &CaptchaInfo) -> Result<CaptchaSolveResult> {
let t0 = Instant::now();
if self.inner.is_empty() {
return Ok(CaptchaSolveResult::failure(
self.method(),
t0.elapsed().as_millis() as u64,
));
}
use futures_util::stream::{FuturesUnordered, StreamExt};
type RaceItem<'a> = std::pin::Pin<
Box<
dyn std::future::Future<
Output = (Arc<dyn CaptchaSolver>, Result<CaptchaSolveResult>),
> + Send
+ 'a,
>,
>;
let mut tasks: FuturesUnordered<RaceItem<'_>> = FuturesUnordered::new();
for solver in &self.inner {
if !solver.supports(&info.kind) {
continue;
}
let s_clone = solver.clone();
let s_call = solver.clone();
tasks.push(Box::pin(async move {
let r = s_call.solve(page, info).await;
(s_clone, r)
}));
}
if tasks.is_empty() {
return Ok(CaptchaSolveResult::failure(
self.method(),
t0.elapsed().as_millis() as u64,
));
}
let mut last_err: Option<anyhow::Error> = None;
let mut last_failure: Option<CaptchaSolveResult> = None;
while let Some((solver, result)) = tasks.next().await {
match result {
Ok(r) if r.success => {
tracing::debug!(winner = solver.name(), "race winner");
return Ok(r);
}
Ok(r) => {
last_failure = Some(r);
}
Err(e) => {
last_err = Some(e);
}
}
}
if let Some(r) = last_failure {
return Ok(r);
}
if let Some(e) = last_err {
return Err(e);
}
Ok(CaptchaSolveResult::failure(
self.method(),
t0.elapsed().as_millis() as u64,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::captcha_detect::DetectedCaptcha;
struct StubSolver {
name: &'static str,
kind_supports: bool,
outcome: bool,
}
#[async_trait]
impl CaptchaSolver for StubSolver {
fn name(&self) -> &'static str {
self.name
}
fn method(&self) -> SolveMethod {
SolveMethod::ThirdPartyService
}
fn supports(&self, _kind: &DetectedCaptcha) -> bool {
self.kind_supports
}
async fn solve(&self, _page: &Page, _info: &CaptchaInfo) -> Result<CaptchaSolveResult> {
Ok(CaptchaSolveResult {
solution: format!("from-{}", self.name),
confidence: 1.0,
method: SolveMethod::ThirdPartyService,
time_ms: 1,
success: self.outcome,
screenshot: None,
cookies: Vec::new(),
})
}
}
#[test]
fn empty_race_method_is_crowd_sourced() {
let r = RacingSolver::new("empty");
assert_eq!(r.method(), SolveMethod::CrowdSourced);
}
#[test]
fn race_inherits_first_solver_method() {
let r = RacingSolver::new("race").with(StubSolver {
name: "a",
kind_supports: true,
outcome: true,
});
assert_eq!(r.method(), SolveMethod::ThirdPartyService);
}
#[test]
fn race_supports_union_of_inner() {
let r = RacingSolver::new("race")
.with(StubSolver {
name: "a",
kind_supports: false,
outcome: true,
})
.with(StubSolver {
name: "b",
kind_supports: true,
outcome: true,
});
assert!(r.supports(&DetectedCaptcha::Turnstile));
}
#[test]
fn race_supports_false_when_no_inner_supports() {
let r = RacingSolver::new("race")
.with(StubSolver {
name: "a",
kind_supports: false,
outcome: true,
})
.with(StubSolver {
name: "b",
kind_supports: false,
outcome: true,
});
assert!(!r.supports(&DetectedCaptcha::Turnstile));
}
#[test]
fn name_propagates() {
let r = RacingSolver::new("MyRace");
assert_eq!(r.name(), "MyRace");
}
}