1use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(Debug, Clone)]
8pub struct CaptchaConfig {
9 pub api_key: String,
10 pub client_id: u32,
11}
12
13#[derive(Debug, Serialize)]
14pub struct CreateTaskRequest {
15 #[serde(rename = "clientKey")]
16 pub client_key: String,
17 pub task: CaptchaTask,
18}
19
20#[derive(Debug, Serialize)]
21#[serde(tag = "type")]
22pub enum CaptchaTask {
23 #[serde(rename = "HCaptchaTaskProxyless")]
24 HCaptchaProxyless {
25 #[serde(rename = "websiteURL")]
26 website_url: String,
27 #[serde(rename = "websiteKey")]
28 website_key: String,
29 },
30 #[serde(rename = "NoCaptchaTaskProxyless")]
31 ReCaptchaV2Proxyless {
32 #[serde(rename = "websiteURL")]
33 website_url: String,
34 #[serde(rename = "websiteKey")]
35 website_key: String,
36 },
37 #[serde(rename = "RecaptchaV3TaskProxyless")]
38 ReCaptchaV3Proxyless {
39 #[serde(rename = "websiteURL")]
40 website_url: String,
41 #[serde(rename = "websiteKey")]
42 website_key: String,
43 #[serde(rename = "minScore")]
44 min_score: f32,
45 #[serde(rename = "pageAction")]
46 page_action: String,
47 },
48}
49
50#[derive(Debug, Deserialize)]
51pub struct CreateTaskResponse {
52 #[serde(rename = "errorId")]
53 pub error_id: u32,
54 #[serde(rename = "errorCode")]
55 pub error_code: Option<String>,
56 #[serde(rename = "taskId")]
57 pub task_id: Option<u64>,
58}
59
60#[derive(Debug, Serialize)]
61pub struct GetResultRequest {
62 #[serde(rename = "clientKey")]
63 pub client_key: String,
64 #[serde(rename = "taskId")]
65 pub task_id: u64,
66}
67
68#[derive(Debug, Deserialize)]
69pub struct GetResultResponse {
70 #[serde(rename = "errorId")]
71 pub error_id: u32,
72 #[serde(rename = "errorCode")]
73 pub error_code: Option<String>,
74 pub status: Option<String>,
75 pub solution: Option<CaptchaSolution>,
76}
77
78#[derive(Debug, Deserialize)]
79pub struct CaptchaSolution {
80 #[serde(rename = "gRecaptchaResponse")]
81 pub g_recaptcha_response: Option<String>,
82 #[serde(rename = "gRecaptchaResponseWithoutSpaces")]
83 pub g_recaptcha_response_without_spaces: Option<String>,
84 pub text: Option<String>,
85 #[serde(rename = "expireTime")]
86 pub expire_time: Option<i64>,
87}
88
89pub struct AntiCaptcha {
90 client: reqwest::Client,
91 api_key: String,
92}
93
94impl AntiCaptcha {
95 pub fn new(api_key: String) -> Self {
96 Self {
97 client: reqwest::Client::new(),
98 api_key,
99 }
100 }
101
102 pub async fn solve_hcaptcha(
104 &self,
105 website_url: &str,
106 website_key: &str,
107 ) -> Result<String, Box<dyn std::error::Error>> {
108 self.solve_captcha(CaptchaTask::HCaptchaProxyless {
109 website_url: website_url.to_string(),
110 website_key: website_key.to_string(),
111 })
112 .await
113 }
114
115 pub async fn solve_recaptcha_v2(
117 &self,
118 website_url: &str,
119 website_key: &str,
120 ) -> Result<String, Box<dyn std::error::Error>> {
121 self.solve_captcha(CaptchaTask::ReCaptchaV2Proxyless {
122 website_url: website_url.to_string(),
123 website_key: website_key.to_string(),
124 })
125 .await
126 }
127
128 pub async fn solve_recaptcha_v3(
130 &self,
131 website_url: &str,
132 website_key: &str,
133 page_action: &str,
134 min_score: f32,
135 ) -> Result<String, Box<dyn std::error::Error>> {
136 self.solve_captcha(CaptchaTask::ReCaptchaV3Proxyless {
137 website_url: website_url.to_string(),
138 website_key: website_key.to_string(),
139 min_score,
140 page_action: page_action.to_string(),
141 })
142 .await
143 }
144
145 async fn solve_captcha(&self, task: CaptchaTask) -> Result<String, Box<dyn std::error::Error>> {
147 let create_req = CreateTaskRequest {
149 client_key: self.api_key.clone(),
150 task,
151 };
152
153 let response = self
154 .client
155 .post("https://api.anti-captcha.com/createTask")
156 .json(&create_req)
157 .send()
158 .await?;
159
160 let create_resp: CreateTaskResponse = response.json().await?;
161
162 if create_resp.error_id != 0 {
163 return Err(format!(
164 "Failed to create task: {} - {}",
165 create_resp.error_id,
166 create_resp.error_code.unwrap_or_default()
167 )
168 .into());
169 }
170
171 let task_id = create_resp.task_id.ok_or("No task ID returned")?;
172
173 let max_attempts = 300;
175 for attempt in 0..max_attempts {
176 tokio::time::sleep(Duration::from_millis(500)).await;
177
178 let result_req = GetResultRequest {
179 client_key: self.api_key.clone(),
180 task_id,
181 };
182
183 let response = self
184 .client
185 .post("https://api.anti-captcha.com/getTaskResult")
186 .json(&result_req)
187 .send()
188 .await?;
189
190 let result_resp: GetResultResponse = response.json().await?;
191
192 if result_resp.error_id != 0 {
193 return Err(format!(
194 "Failed to get result: {} - {}",
195 result_resp.error_id,
196 result_resp.error_code.unwrap_or_default()
197 )
198 .into());
199 }
200
201 if result_resp.status.as_deref() == Some("ready") {
202 if let Some(solution) = result_resp.solution {
203 return Ok(solution
204 .g_recaptcha_response
205 .or(solution.g_recaptcha_response_without_spaces)
206 .or(solution.text)
207 .ok_or("No solution in response")?);
208 }
209 return Err("No solution data returned".into());
210 }
211
212 if attempt % 10 == 0 && attempt > 0 {
214 eprintln!(
215 "Captcha solving in progress... ({}/{}s)",
216 attempt / 2,
217 max_attempts / 2
218 );
219 }
220 }
221
222 Err("Captcha solving timeout (5 minutes)".into())
223 }
224
225 pub async fn detect_captcha_on_page(page: &eoka::Page) -> Option<CaptchaInfo> {
227 let hcaptcha_script = r#"
229 (function() {
230 const elem = document.querySelector('[data-sitekey]');
231 if (elem && elem.getAttribute('data-sitekey')) {
232 return elem.getAttribute('data-sitekey');
233 }
234 return null;
235 })()
236 "#;
237
238 if let Ok(result) = page.evaluate::<serde_json::Value>(hcaptcha_script).await {
239 if let Some(key_str) = result.as_str() {
240 if !key_str.is_empty() {
241 return Some(CaptchaInfo {
242 captcha_type: "hcaptcha".to_string(),
243 sitekey: key_str.to_string(),
244 });
245 }
246 }
247 }
248
249 let recaptcha_script = r#"
251 (function() {
252 const scripts = document.querySelectorAll('script');
253 for (const script of scripts) {
254 if (script.src && script.src.includes('recaptcha')) {
255 const matches = document.documentElement.innerHTML.match(/"sitekey"\s*:\s*"([^"]+)"/);
256 if (matches) return matches[1];
257 }
258 }
259 return null;
260 })()
261 "#;
262
263 if let Ok(result) = page.evaluate::<serde_json::Value>(recaptcha_script).await {
264 if let Some(key_str) = result.as_str() {
265 if !key_str.is_empty() {
266 return Some(CaptchaInfo {
267 captcha_type: "recaptcha".to_string(),
268 sitekey: key_str.to_string(),
269 });
270 }
271 }
272 }
273
274 None
275 }
276}
277
278#[derive(Debug, Clone)]
279pub struct CaptchaInfo {
280 pub captcha_type: String,
281 pub sitekey: String,
282}