Skip to main content

capmonstercloud_client/requests/
ten_di.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/// Optional TenDI (Tencent captcha) parameters.
9#[derive(Debug, Clone, Default, Serialize)]
10pub struct TenDiMetadata {
11    /// Link to the captcha script, usually `TCaptcha.js` / `TCaptcha-global.js`
12    /// (`TJNCaptcha*.js` / `TJCaptcha*.js` for the second version).
13    #[serde(rename = "captchaUrl", skip_serializing_if = "Option::is_none")]
14    pub captcha_url: Option<String>,
15    /// `Aid` value obtained from the target website; single-use, fetch a new one for every task.
16    /// Not used on all websites: omit it if the site does not return an Aid.
17    #[serde(rename = "aidEncrypted", skip_serializing_if = "Option::is_none")]
18    pub aid_encrypted: Option<String>,
19}
20
21impl TenDiMetadata {
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    pub fn with_captcha_url(mut self, value: impl Into<String>) -> Self {
27        self.captcha_url = Some(value.into());
28        self
29    }
30
31    pub fn with_aid_encrypted(mut self, value: impl Into<String>) -> Self {
32        self.aid_encrypted = Some(value.into());
33        self
34    }
35}
36
37/// TenDI / Tencent captcha (`CustomTask`, class `TenDI`).
38#[derive(Debug, Clone, Serialize)]
39pub struct TenDiCustomTaskRequest {
40    /// Address of the main page where the captcha is solved.
41    #[serde(rename = "websiteURL")]
42    pub website_url: String,
43    /// `captchaAppId`, the `aid` value (e.g. `"189123456"`), unique per website.
44    #[serde(rename = "websiteKey")]
45    pub website_key: String,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub metadata: Option<TenDiMetadata>,
48    #[serde(flatten, skip_serializing_if = "Option::is_none")]
49    pub proxy: Option<ProxyInfo>,
50    /// Browser User-Agent (current Windows UA supported by CapMonster Cloud).
51    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
52    pub user_agent: Option<String>,
53}
54
55impl TenDiCustomTaskRequest {
56    pub fn new(website_url: impl Into<String>, website_key: impl Into<String>) -> Self {
57        Self {
58            website_url: website_url.into(),
59            website_key: website_key.into(),
60            metadata: None,
61            proxy: None,
62            user_agent: None,
63        }
64    }
65
66    pub fn with_metadata(mut self, value: TenDiMetadata) -> Self {
67        self.metadata = Some(value);
68        self
69    }
70
71    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
72        self.proxy = Some(proxy);
73        self
74    }
75
76    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
77        self.user_agent = Some(value.into());
78        self
79    }
80}
81
82impl CaptchaRequest for TenDiCustomTaskRequest {
83    const TASK_TYPE: &'static str = "CustomTask";
84    const CLASS: Option<&'static str> = Some("TenDI");
85
86    fn timeouts(&self) -> GetResultTimeouts {
87        GetResultTimeouts::secs(1.0, 10.0, 3.0, 180.0)
88    }
89
90    fn validate(&self) -> Result<()> {
91        require_non_empty("websiteURL", &self.website_url)?;
92        require_non_empty("websiteKey", &self.website_key)?;
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::proxy::ProxyType;
101    use serde_json::json;
102
103    #[test]
104    fn minimal_task() {
105        let task = TenDiCustomTaskRequest::new("https://example.com", "189123456")
106            .to_task()
107            .unwrap();
108        assert_eq!(
109            task,
110            json!({"type": "CustomTask", "class": "TenDI", "websiteURL": "https://example.com", "websiteKey": "189123456"})
111        );
112    }
113
114    #[test]
115    fn full_task() {
116        let task = TenDiCustomTaskRequest::new("https://example.com", "189123456")
117            .with_metadata(
118                TenDiMetadata::new()
119                    .with_captcha_url("https://x/TCaptcha.js")
120                    .with_aid_encrypted("AID"),
121            )
122            .with_proxy(ProxyInfo::new(ProxyType::Http, "1.2.3.4", 80))
123            .with_user_agent("UA")
124            .to_task()
125            .unwrap();
126        assert_eq!(
127            task,
128            json!({
129                "type": "CustomTask", "class": "TenDI", "websiteURL": "https://example.com", "websiteKey": "189123456",
130                "metadata": {"captchaUrl": "https://x/TCaptcha.js", "aidEncrypted": "AID"},
131                "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 80, "userAgent": "UA"
132            })
133        );
134    }
135
136    #[test]
137    fn empty_required_strings_are_rejected() {
138        let err = TenDiCustomTaskRequest::new("", "KEY")
139            .to_task()
140            .unwrap_err();
141        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
142        let err = TenDiCustomTaskRequest::new("https://example.com", "")
143            .to_task()
144            .unwrap_err();
145        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteKey")));
146    }
147}