Skip to main content

capmonstercloud_client/requests/
yidun.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/// NetEase Yidun captcha (`YidunTask`). Setting `challenge` selects the Enterprise variant.
9#[derive(Debug, Clone, Serialize)]
10pub struct YidunRequest {
11    /// Full URL of the page with the captcha.
12    #[serde(rename = "websiteURL")]
13    pub website_url: String,
14    /// The `siteKey` value found on the page.
15    #[serde(rename = "websiteKey")]
16    pub website_key: String,
17    #[serde(flatten, skip_serializing_if = "Option::is_none")]
18    pub proxy: Option<ProxyInfo>,
19    /// Browser User-Agent (use a current Windows one).
20    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
21    pub user_agent: Option<String>,
22    /// Full URL (`https://...`) of the JS file that loads the captcha. Recommended when
23    /// `challenge`, `hcg` or `hct` are present on the site.
24    #[serde(rename = "yidunGetLib", skip_serializing_if = "Option::is_none")]
25    pub yidun_get_lib: Option<String>,
26    /// Yidun API server subdomain; must differ from `c.dun.163.com` / `c-v6.dun.163.com`
27    /// and be given without `https://`. Needed for custom (often Enterprise) servers.
28    #[serde(
29        rename = "yidunApiServerSubdomain",
30        skip_serializing_if = "Option::is_none"
31    )]
32    pub yidun_api_server_subdomain: Option<String>,
33    /// Unique identifier of the current captcha; indicates the Enterprise captcha.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub challenge: Option<String>,
36    /// Captcha hash used in the request; obtained together with `challenge`.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub hcg: Option<String>,
39    /// Numeric timestamp used in Enterprise validation.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub hct: Option<i64>,
42}
43
44impl YidunRequest {
45    pub fn new(website_url: impl Into<String>, website_key: impl Into<String>) -> Self {
46        Self {
47            website_url: website_url.into(),
48            website_key: website_key.into(),
49            proxy: None,
50            user_agent: None,
51            yidun_get_lib: None,
52            yidun_api_server_subdomain: None,
53            challenge: None,
54            hcg: None,
55            hct: None,
56        }
57    }
58
59    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
60        self.proxy = Some(proxy);
61        self
62    }
63
64    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
65        self.user_agent = Some(value.into());
66        self
67    }
68
69    pub fn with_yidun_get_lib(mut self, value: impl Into<String>) -> Self {
70        self.yidun_get_lib = Some(value.into());
71        self
72    }
73
74    pub fn with_yidun_api_server_subdomain(mut self, value: impl Into<String>) -> Self {
75        self.yidun_api_server_subdomain = Some(value.into());
76        self
77    }
78
79    pub fn with_challenge(mut self, value: impl Into<String>) -> Self {
80        self.challenge = Some(value.into());
81        self
82    }
83
84    pub fn with_hcg(mut self, value: impl Into<String>) -> Self {
85        self.hcg = Some(value.into());
86        self
87    }
88
89    pub fn with_hct(mut self, value: i64) -> Self {
90        self.hct = Some(value);
91        self
92    }
93}
94
95impl CaptchaRequest for YidunRequest {
96    const TASK_TYPE: &'static str = "YidunTask";
97
98    fn timeouts(&self) -> GetResultTimeouts {
99        GetResultTimeouts::secs(1.0, 10.0, 3.0, 180.0)
100    }
101
102    fn validate(&self) -> Result<()> {
103        require_non_empty("websiteURL", &self.website_url)?;
104        require_non_empty("websiteKey", &self.website_key)?;
105        Ok(())
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::proxy::ProxyType;
113    use serde_json::json;
114
115    #[test]
116    fn minimal_task() {
117        let task = YidunRequest::new("https://example.com", "KEY")
118            .to_task()
119            .unwrap();
120        assert_eq!(
121            task,
122            json!({"type": "YidunTask", "websiteURL": "https://example.com", "websiteKey": "KEY"})
123        );
124    }
125
126    #[test]
127    fn full_task() {
128        let task = YidunRequest::new("https://example.com", "KEY")
129            .with_proxy(ProxyInfo::new(ProxyType::Http, "1.2.3.4", 80))
130            .with_user_agent("UA")
131            .with_yidun_get_lib("https://cstaticdun.126.net/load.min.js")
132            .with_yidun_api_server_subdomain("c.example.com")
133            .with_challenge("ch")
134            .with_hcg("hcg")
135            .with_hct(1700000000000)
136            .to_task()
137            .unwrap();
138        assert_eq!(
139            task,
140            json!({"type": "YidunTask", "websiteURL": "https://example.com", "websiteKey": "KEY",
141                   "userAgent": "UA", "yidunGetLib": "https://cstaticdun.126.net/load.min.js",
142                   "yidunApiServerSubdomain": "c.example.com", "challenge": "ch", "hcg": "hcg",
143                   "hct": 1700000000000i64,
144                   "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 80})
145        );
146    }
147
148    #[test]
149    fn empty_required_strings_are_rejected() {
150        let err = YidunRequest::new("", "KEY").to_task().unwrap_err();
151        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
152        let err = YidunRequest::new("https://example.com", "")
153            .to_task()
154            .unwrap_err();
155        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteKey")));
156    }
157}