Skip to main content

capmonstercloud_client/requests/
recaptcha_v2.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/// reCAPTCHA v2 (`RecaptchaV2Task`). Without `proxy` the task is solved from CapMonster's own IPs.
9#[derive(Debug, Clone, Serialize)]
10pub struct RecaptchaV2Request {
11    /// URL of the page with the captcha.
12    #[serde(rename = "websiteURL")]
13    pub website_url: String,
14    /// reCAPTCHA site key.
15    #[serde(rename = "websiteKey")]
16    pub website_key: String,
17    #[serde(flatten, skip_serializing_if = "Option::is_none")]
18    pub proxy: Option<ProxyInfo>,
19    /// One-time `data-s` token of some custom reCAPTCHA v2 implementations.
20    #[serde(
21        rename = "recaptchaDataSValue",
22        skip_serializing_if = "Option::is_none"
23    )]
24    pub data_s_value: Option<String>,
25    /// Current browser User-Agent (Windows).
26    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
27    pub user_agent: Option<String>,
28    /// Cookies as `"name1=value1; name2=value2"`.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub cookies: Option<String>,
31    #[serde(rename = "isInvisible", skip_serializing_if = "Option::is_none")]
32    pub is_invisible: Option<bool>,
33}
34
35impl RecaptchaV2Request {
36    pub fn new(website_url: impl Into<String>, website_key: impl Into<String>) -> Self {
37        Self {
38            website_url: website_url.into(),
39            website_key: website_key.into(),
40            proxy: None,
41            data_s_value: None,
42            user_agent: None,
43            cookies: None,
44            is_invisible: None,
45        }
46    }
47
48    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
49        self.proxy = Some(proxy);
50        self
51    }
52
53    pub fn with_data_s_value(mut self, value: impl Into<String>) -> Self {
54        self.data_s_value = Some(value.into());
55        self
56    }
57
58    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
59        self.user_agent = Some(value.into());
60        self
61    }
62
63    pub fn with_cookies(mut self, value: impl Into<String>) -> Self {
64        self.cookies = Some(value.into());
65        self
66    }
67
68    pub fn with_is_invisible(mut self, value: bool) -> Self {
69        self.is_invisible = Some(value);
70        self
71    }
72}
73
74impl CaptchaRequest for RecaptchaV2Request {
75    const TASK_TYPE: &'static str = "RecaptchaV2Task";
76
77    fn timeouts(&self) -> GetResultTimeouts {
78        GetResultTimeouts::secs(1.0, 10.0, 3.0, 180.0)
79    }
80
81    fn validate(&self) -> Result<()> {
82        require_non_empty("websiteURL", &self.website_url)?;
83        require_non_empty("websiteKey", &self.website_key)?;
84        Ok(())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::proxy::{ProxyInfo, ProxyType};
92    use serde_json::json;
93
94    #[test]
95    fn minimal_task() {
96        let task = RecaptchaV2Request::new("https://example.com", "KEY")
97            .to_task()
98            .unwrap();
99        assert_eq!(
100            task,
101            json!({"type": "RecaptchaV2Task", "websiteURL": "https://example.com", "websiteKey": "KEY"})
102        );
103    }
104
105    #[test]
106    fn full_task_flattens_proxy_and_renames_fields() {
107        let task = RecaptchaV2Request::new("https://example.com", "KEY")
108            .with_proxy(ProxyInfo::new(ProxyType::Socks5, "1.2.3.4", 1080).with_auth("u", "p"))
109            .with_data_s_value("S")
110            .with_user_agent("UA")
111            .with_cookies("a=b")
112            .with_is_invisible(true)
113            .to_task()
114            .unwrap();
115        assert_eq!(
116            task,
117            json!({
118                "type": "RecaptchaV2Task", "websiteURL": "https://example.com", "websiteKey": "KEY",
119                "proxyType": "socks5", "proxyAddress": "1.2.3.4", "proxyPort": 1080,
120                "proxyLogin": "u", "proxyPassword": "p",
121                "recaptchaDataSValue": "S", "userAgent": "UA", "cookies": "a=b", "isInvisible": true
122            })
123        );
124    }
125
126    #[test]
127    fn empty_required_strings_are_rejected() {
128        let err = RecaptchaV2Request::new("", "KEY").to_task().unwrap_err();
129        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
130        let err = RecaptchaV2Request::new("https://example.com", "")
131            .to_task()
132            .unwrap_err();
133        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteKey")));
134    }
135}