Skip to main content

capmonstercloud_client/requests/
funcaptcha.rs

1use serde::Serialize;
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::Result;
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8/// FunCaptcha / Arkose Labs (`FunCaptchaTask`).
9///
10/// The docs require your own proxy for this task (use the same proxy to obtain the captcha
11/// parameters and to create the task), so `proxy` is mandatory.
12#[derive(Debug, Clone, Serialize)]
13pub struct FuncaptchaRequest {
14    /// URL of the page where the captcha is located.
15    #[serde(rename = "websiteURL")]
16    pub website_url: String,
17    /// FunCaptcha public key (`pk`), e.g. `EX72CCFB-26EX-40E5-91E6-85EX70BE98ED`.
18    #[serde(rename = "websitePublicKey")]
19    pub website_public_key: String,
20    /// Mandatory proxy used by the worker.
21    #[serde(flatten)]
22    pub proxy: ProxyInfo,
23    /// Arkose Labs subdomain (`surl`) without `https://`, only if it differs from
24    /// `client-api.arkoselabs.com`.
25    #[serde(
26        rename = "funcaptchaApiJSSubdomain",
27        skip_serializing_if = "Option::is_none"
28    )]
29    pub funcaptcha_api_js_subdomain: Option<String>,
30    /// Additional parameter, required if the site uses `data[blob]`, e.g. `{"blob":"..."}`.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub data: Option<String>,
33    /// Cookies as `"name1=value1; name2=value2"`.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub cookies: Option<String>,
36    /// Browser User-Agent (use the value supported by CapMonster Cloud).
37    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
38    pub user_agent: Option<String>,
39}
40
41impl FuncaptchaRequest {
42    pub fn new(
43        website_url: impl Into<String>,
44        website_public_key: impl Into<String>,
45        proxy: ProxyInfo,
46    ) -> Self {
47        Self {
48            website_url: website_url.into(),
49            website_public_key: website_public_key.into(),
50            proxy,
51            funcaptcha_api_js_subdomain: None,
52            data: None,
53            cookies: None,
54            user_agent: None,
55        }
56    }
57
58    pub fn with_funcaptcha_api_js_subdomain(mut self, value: impl Into<String>) -> Self {
59        self.funcaptcha_api_js_subdomain = Some(value.into());
60        self
61    }
62
63    pub fn with_data(mut self, value: impl Into<String>) -> Self {
64        self.data = Some(value.into());
65        self
66    }
67
68    pub fn with_cookies(mut self, value: impl Into<String>) -> Self {
69        self.cookies = Some(value.into());
70        self
71    }
72
73    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
74        self.user_agent = Some(value.into());
75        self
76    }
77}
78
79impl CaptchaRequest for FuncaptchaRequest {
80    const TASK_TYPE: &'static str = "FunCaptchaTask";
81
82    fn timeouts(&self) -> GetResultTimeouts {
83        GetResultTimeouts::secs(1.0, 10.0, 1.0, 80.0)
84    }
85
86    fn validate(&self) -> Result<()> {
87        require_non_empty("websiteURL", &self.website_url)?;
88        require_non_empty("websitePublicKey", &self.website_public_key)?;
89        Ok(())
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::proxy::ProxyType;
97    use serde_json::json;
98
99    fn proxy() -> ProxyInfo {
100        ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080)
101    }
102
103    #[test]
104    fn minimal_task() {
105        let task = FuncaptchaRequest::new("https://example.com", "PK", proxy())
106            .to_task()
107            .unwrap();
108        assert_eq!(
109            task,
110            json!({"type": "FunCaptchaTask", "websiteURL": "https://example.com",
111                   "websitePublicKey": "PK",
112                   "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080})
113        );
114    }
115
116    #[test]
117    fn full_task() {
118        let task = FuncaptchaRequest::new("https://example.com", "PK", proxy().with_auth("u", "p"))
119            .with_funcaptcha_api_js_subdomain("x-api.arkoselabs.com")
120            .with_data("{\"blob\":\"B\"}")
121            .with_cookies("a=b")
122            .with_user_agent("UA")
123            .to_task()
124            .unwrap();
125        assert_eq!(
126            task,
127            json!({"type": "FunCaptchaTask", "websiteURL": "https://example.com",
128                   "websitePublicKey": "PK",
129                   "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
130                   "proxyLogin": "u", "proxyPassword": "p",
131                   "funcaptchaApiJSSubdomain": "x-api.arkoselabs.com",
132                   "data": "{\"blob\":\"B\"}", "cookies": "a=b", "userAgent": "UA"})
133        );
134    }
135
136    #[test]
137    fn empty_required_strings_are_rejected() {
138        let err = FuncaptchaRequest::new("", "PK", proxy())
139            .to_task()
140            .unwrap_err();
141        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
142        let err = FuncaptchaRequest::new("https://example.com", "", proxy())
143            .to_task()
144            .unwrap_err();
145        assert!(
146            matches!(err, crate::error::Error::Validation(ref m) if m.contains("websitePublicKey"))
147        );
148    }
149}