use base64::Engine;
use serde_json::Value;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use tokio::time::sleep;
use crate::api::ApiClient;
use crate::error::{Result, TwoCaptchaError};
use crate::types::{
AudioLanguage, Balance, CaptchaResult, ExtendedResponse, Proxy, RecaptchaVersion,
};
use crate::utils::Utils;
#[derive(Debug, Clone, Default)]
pub struct TwoCaptchaConfig {
pub soft_id: Option<u32>,
pub callback: Option<String>,
pub default_timeout: Option<Duration>,
pub recaptcha_timeout: Option<Duration>,
pub polling_interval: Option<Duration>,
pub server: Option<String>,
pub extended_response: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct TwoCaptcha {
api_key: String,
soft_id: Option<u32>,
callback: Option<String>,
default_timeout: Duration,
recaptcha_timeout: Duration,
polling_interval: Duration,
api_client: ApiClient,
max_files: usize,
extended_response: bool,
}
impl TwoCaptcha {
pub fn new(api_key: String, config: TwoCaptchaConfig) -> Self {
Self {
api_key,
soft_id: config.soft_id.or(Some(4580)),
callback: config.callback,
default_timeout: config.default_timeout.unwrap_or(Duration::from_secs(120)),
recaptcha_timeout: config.recaptcha_timeout.unwrap_or(Duration::from_secs(600)),
polling_interval: config.polling_interval.unwrap_or(Duration::from_secs(10)),
api_client: ApiClient::new(config.server),
max_files: 9,
extended_response: config.extended_response.unwrap_or(false),
}
}
pub async fn normal(
&self,
file: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let method = Utils::get_method(file).await?;
let mut all_params = method;
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn audio(
&self,
file: &str,
lang: AudioLanguage,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let body = if !file.contains('.') && file.len() > 50 {
file.to_string()
} else if file.ends_with(".mp3") && file.starts_with("http") {
let response = reqwest::get(file).await?;
if response.status() != 200 {
return Err(TwoCaptchaError::Validation(format!(
"File could not be downloaded from url: {file}"
)));
}
let content = response.bytes().await?;
base64::engine::general_purpose::STANDARD.encode(&content)
} else if file.ends_with(".mp3") {
let content = tokio::fs::read(file).await?;
base64::engine::general_purpose::STANDARD.encode(&content)
} else {
return Err(TwoCaptchaError::Validation(
"File extension is not .mp3 or it is not a base64 string.".to_string(),
));
};
let mut all_params = HashMap::new();
all_params.insert("body".to_string(), body);
all_params.insert("method".to_string(), "audio".to_string());
all_params.insert("lang".to_string(), lang.as_str().to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn text(
&self,
text: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("text".to_string(), text.to_string());
all_params.insert("method".to_string(), "post".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn recaptcha(
&self,
sitekey: &str,
url: &str,
version: Option<RecaptchaVersion>,
enterprise: Option<bool>,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("googlekey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "userrecaptcha".to_string());
all_params.insert(
"version".to_string(),
version.unwrap_or(RecaptchaVersion::V2).as_str().to_string(),
);
all_params.insert(
"enterprise".to_string(),
if enterprise.unwrap_or(false) {
"1"
} else {
"0"
}
.to_string(),
);
if let Some(p) = params {
all_params.extend(p);
}
self.solve(Some(self.recaptcha_timeout), None, all_params)
.await
}
pub async fn funcaptcha(
&self,
sitekey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("publickey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "funcaptcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn geetest(
&self,
gt: &str,
challenge: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("gt".to_string(), gt.to_string());
all_params.insert("challenge".to_string(), challenge.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "geetest".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn hcaptcha(
&self,
sitekey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("sitekey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "hcaptcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn keycaptcha(
&self,
s_s_c_user_id: &str,
s_s_c_session_id: &str,
s_s_c_web_server_sign: &str,
s_s_c_web_server_sign2: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("s_s_c_user_id".to_string(), s_s_c_user_id.to_string());
all_params.insert("s_s_c_session_id".to_string(), s_s_c_session_id.to_string());
all_params.insert(
"s_s_c_web_server_sign".to_string(),
s_s_c_web_server_sign.to_string(),
);
all_params.insert(
"s_s_c_web_server_sign2".to_string(),
s_s_c_web_server_sign2.to_string(),
);
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "keycaptcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn capy(
&self,
sitekey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("captchakey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "capy".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn grid(
&self,
file: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let method = Utils::get_method(file).await?;
let mut all_params = method;
all_params.insert("recaptcha".to_string(), "1".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn canvas(
&self,
file: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let provided_params = params.clone().unwrap_or_default();
if !provided_params.contains_key("hintText") && !provided_params.contains_key("hintImg") {
return Err(TwoCaptchaError::Validation(
"parameters required: hintText and/or hintImg".to_string(),
));
}
let method = Utils::get_method(file).await?;
let mut all_params = method;
all_params.insert("recaptcha".to_string(), "1".to_string());
all_params.insert("canvas".to_string(), "1".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn coordinates(
&self,
file: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let method = Utils::get_method(file).await?;
let mut all_params = method;
all_params.insert("coordinatescaptcha".to_string(), "1".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn rotate(
&self,
files: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let file_method = Utils::get_method(files).await?;
let mut all_params = HashMap::new();
if let Some(file) = file_method.get("file") {
all_params.insert("file".to_string(), file.clone());
}
all_params.insert("method".to_string(), "rotatecaptcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn rotate_multiple(
&self,
files: Vec<String>,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let extracted_files = Utils::extract_files(files, self.max_files)?;
let mut all_params = HashMap::new();
all_params.insert("method".to_string(), "rotatecaptcha".to_string());
all_params.extend(extracted_files);
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn geetest_v4(
&self,
captcha_id: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("captcha_id".to_string(), captcha_id.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "geetest_v4".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn lemin(
&self,
captcha_id: &str,
div_id: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("captcha_id".to_string(), captcha_id.to_string());
all_params.insert("div_id".to_string(), div_id.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "lemin".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn atb_captcha(
&self,
app_id: &str,
api_server: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("app_id".to_string(), app_id.to_string());
all_params.insert("api_server".to_string(), api_server.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "atb_captcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn turnstile(
&self,
sitekey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("sitekey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "turnstile".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn amazon_waf(
&self,
sitekey: &str,
iv: &str,
context: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("sitekey".to_string(), sitekey.to_string());
all_params.insert("iv".to_string(), iv.to_string());
all_params.insert("context".to_string(), context.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "amazon_waf".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn mtcaptcha(
&self,
sitekey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("sitekey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "mt_captcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn friendly_captcha(
&self,
sitekey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("sitekey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "friendly_captcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn tencent(
&self,
app_id: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("app_id".to_string(), app_id.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "tencent".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn cutcaptcha(
&self,
misery_key: &str,
apikey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("misery_key".to_string(), misery_key.to_string());
all_params.insert("api_key".to_string(), apikey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "cutcaptcha".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn datadome(
&self,
captcha_url: &str,
pageurl: &str,
user_agent: &str,
proxy: Proxy,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("method".to_string(), "datadome".to_string());
all_params.insert("captcha_url".to_string(), captcha_url.to_string());
all_params.insert("pageurl".to_string(), pageurl.to_string());
all_params.insert("userAgent".to_string(), user_agent.to_string());
let proxy_json = serde_json::to_string(&proxy)?;
all_params.insert("proxy".to_string(), proxy_json);
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn cybersiara(
&self,
master_url_id: &str,
pageurl: &str,
user_agent: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("method".to_string(), "cybersiara".to_string());
all_params.insert("master_url_id".to_string(), master_url_id.to_string());
all_params.insert("pageurl".to_string(), pageurl.to_string());
all_params.insert("userAgent".to_string(), user_agent.to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn yandex_smart(
&self,
sitekey: &str,
url: &str,
params: Option<HashMap<String, String>>,
) -> Result<CaptchaResult> {
let mut all_params = HashMap::new();
all_params.insert("sitekey".to_string(), sitekey.to_string());
all_params.insert("url".to_string(), url.to_string());
all_params.insert("method".to_string(), "yandex".to_string());
if let Some(p) = params {
all_params.extend(p);
}
self.solve(None, None, all_params).await
}
pub async fn solve(
&self,
timeout: Option<Duration>,
polling_interval: Option<Duration>,
params: HashMap<String, String>,
) -> Result<CaptchaResult> {
let id = self.send(params).await?;
let mut result = CaptchaResult {
captcha_id: id.clone(),
code: None,
extended: None,
};
if self.callback.is_none() {
let timeout = timeout.unwrap_or(self.default_timeout);
let sleep_interval = polling_interval.unwrap_or(self.polling_interval);
let code = self.wait_result(&id, timeout, sleep_interval).await?;
if self.extended_response {
if let Ok(extended) = serde_json::from_str::<ExtendedResponse>(&code) {
let mut extended_map = HashMap::new();
extended_map.insert(
"status".to_string(),
serde_json::Value::Number(extended.status.into()),
);
if let Some(request) = extended.request {
extended_map.insert("code".to_string(), serde_json::Value::String(request));
}
if let Some(cookies) = extended.cookies {
extended_map.insert("cookies".to_string(), serde_json::to_value(cookies)?);
}
extended_map.extend(extended.additional);
result.extended = Some(extended_map);
} else {
result.code = Some(code);
}
} else {
result.code = Some(code);
}
}
Ok(result)
}
async fn wait_result(
&self,
id: &str,
timeout: Duration,
polling_interval: Duration,
) -> Result<String> {
let start = Instant::now();
while start.elapsed() < timeout {
match self.get_result(id).await {
Ok(result) => return Ok(result),
Err(TwoCaptchaError::Network(_)) => {
sleep(polling_interval).await;
continue;
}
Err(e) => return Err(e),
}
}
Err(TwoCaptchaError::Timeout(format!(
"timeout {} exceeded",
timeout.as_secs()
)))
}
async fn send(&self, mut params: HashMap<String, String>) -> Result<String> {
params = self.default_params(params);
params = Utils::rename_params(params);
let (params, files) = Utils::check_hint_img(params, HashMap::new()).await?;
let response = if files.is_empty() {
self.api_client.in_(None, params).await?
} else {
let mut file_bytes = HashMap::new();
for (key, path) in files {
let content = tokio::fs::read(&path).await?;
file_bytes.insert(key, content);
}
self.api_client.in_(Some(file_bytes), params).await?
};
if !response.starts_with("OK|") {
return Err(TwoCaptchaError::Api(format!(
"cannot recognize response {response}"
)));
}
Ok(response[3..].to_string())
}
async fn get_result(&self, id: &str) -> Result<String> {
let mut params = HashMap::new();
params.insert("key".to_string(), self.api_key.clone());
params.insert("action".to_string(), "get".to_string());
params.insert("id".to_string(), id.to_string());
if self.extended_response {
params.insert("json".to_string(), "1".to_string());
}
let response = self.api_client.res(params).await?;
if self.extended_response {
let response_data: Value = serde_json::from_str(&response)?;
if response_data.get("status").and_then(|v| v.as_i64()) == Some(0) {
return Err(TwoCaptchaError::Network("CAPTCHA_NOT_READY".to_string()));
}
if response_data.get("status").and_then(|v| v.as_i64()) != Some(1) {
return Err(TwoCaptchaError::Api(format!(
"Unexpected status in response: {response}"
)));
}
Ok(response)
} else {
if response == "CAPCHA_NOT_READY" {
return Err(TwoCaptchaError::Network("CAPTCHA_NOT_READY".to_string()));
}
if !response.starts_with("OK|") {
return Err(TwoCaptchaError::Api(format!(
"cannot recognize response {response}"
)));
}
Ok(response[3..].to_string())
}
}
pub async fn balance(&self) -> Result<Balance> {
let mut params = HashMap::new();
params.insert("key".to_string(), self.api_key.clone());
params.insert("action".to_string(), "getbalance".to_string());
let response = self.api_client.res(params).await?;
let balance: f64 = response
.parse()
.map_err(|_| TwoCaptchaError::Api(format!("Invalid balance response: {response}")))?;
Ok(Balance(balance))
}
pub async fn report(&self, id: &str, correct: bool) -> Result<()> {
let mut params = HashMap::new();
params.insert("key".to_string(), self.api_key.clone());
params.insert(
"action".to_string(),
if correct { "reportgood" } else { "reportbad" }.to_string(),
);
params.insert("id".to_string(), id.to_string());
self.api_client.res(params).await?;
Ok(())
}
fn default_params(&self, mut params: HashMap<String, String>) -> HashMap<String, String> {
params.insert("key".to_string(), self.api_key.clone());
if let Some(callback) = &self.callback {
params.insert("callback".to_string(), callback.clone());
}
if let Some(soft_id) = self.soft_id {
params.insert("softId".to_string(), soft_id.to_string());
}
params
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_twocaptcha_creation() {
let config = TwoCaptchaConfig {
soft_id: Some(1234),
..Default::default()
};
let client = TwoCaptcha::new("test_key".to_string(), config);
assert_eq!(client.api_key, "test_key");
assert_eq!(client.soft_id, Some(1234));
assert_eq!(client.max_files, 9);
}
}