capmonstercloud_client/requests/
tspd.rs1use serde::Serialize;
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::{Error, Result};
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8#[derive(Debug, Clone, Serialize)]
10pub struct TspdMetadata {
11 #[serde(rename = "tspdCookie")]
13 pub tspd_cookie: String,
14 #[serde(rename = "htmlPageBase64")]
16 pub html_page_base64: String,
17}
18
19impl TspdMetadata {
20 pub fn new(tspd_cookie: impl Into<String>, html_page_base64: impl Into<String>) -> Self {
21 Self {
22 tspd_cookie: tspd_cookie.into(),
23 html_page_base64: html_page_base64.into(),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Serialize)]
30pub struct TspdCustomTaskRequest {
31 #[serde(rename = "websiteURL")]
33 pub website_url: String,
34 pub metadata: TspdMetadata,
35 #[serde(flatten)]
37 pub proxy: ProxyInfo,
38 #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
41 pub user_agent: Option<String>,
42}
43
44impl TspdCustomTaskRequest {
45 pub fn new(website_url: impl Into<String>, metadata: TspdMetadata, proxy: ProxyInfo) -> Self {
46 Self {
47 website_url: website_url.into(),
48 metadata,
49 proxy,
50 user_agent: None,
51 }
52 }
53
54 pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
55 self.user_agent = Some(value.into());
56 self
57 }
58}
59
60impl CaptchaRequest for TspdCustomTaskRequest {
61 const TASK_TYPE: &'static str = "CustomTask";
62 const CLASS: Option<&'static str> = Some("tspd");
63
64 fn timeouts(&self) -> GetResultTimeouts {
65 GetResultTimeouts::secs(1.0, 0.0, 1.0, 60.0)
66 }
67
68 fn validate(&self) -> Result<()> {
69 require_non_empty("websiteURL", &self.website_url)?;
70 require_non_empty("metadata.tspdCookie", &self.metadata.tspd_cookie)?;
71 require_non_empty("metadata.htmlPageBase64", &self.metadata.html_page_base64)?;
72 if let Some(login) = &self.proxy.proxy_login {
73 if login.contains([';', '|', ')']) {
74 return Err(Error::Validation(
75 "proxy login must not contain the characters ';', '|', ')' for TSPD".into(),
76 ));
77 }
78 }
79 Ok(())
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::proxy::ProxyType;
87 use serde_json::json;
88
89 fn proxy() -> ProxyInfo {
90 ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080).with_auth("u", "p")
91 }
92
93 #[test]
94 fn minimal_task() {
95 let task = TspdCustomTaskRequest::new(
96 "https://example.com",
97 TspdMetadata::new("TS1=a", "PGh0bWw+"),
98 proxy(),
99 )
100 .to_task()
101 .unwrap();
102 assert_eq!(
103 task,
104 json!({
105 "type": "CustomTask", "class": "tspd", "websiteURL": "https://example.com",
106 "metadata": {"tspdCookie": "TS1=a", "htmlPageBase64": "PGh0bWw+"},
107 "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
108 "proxyLogin": "u", "proxyPassword": "p"
109 })
110 );
111 }
112
113 #[test]
114 fn full_task() {
115 let task =
116 TspdCustomTaskRequest::new("https://example.com", TspdMetadata::new("C", "H"), proxy())
117 .with_user_agent("UA")
118 .to_task()
119 .unwrap();
120 assert_eq!(task["userAgent"], "UA");
121 assert_eq!(
122 task["metadata"],
123 json!({"tspdCookie": "C", "htmlPageBase64": "H"})
124 );
125 }
126
127 #[test]
128 fn rejects_forbidden_login_chars() {
129 for bad in ["a;b", "a|b", "a)b"] {
130 let p = ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080).with_auth(bad, "p");
131 let req = TspdCustomTaskRequest::new("https://e.com", TspdMetadata::new("C", "H"), p);
132 assert!(matches!(req.to_task(), Err(Error::Validation(_))), "{bad}");
133 }
134 }
135
136 #[test]
137 fn empty_required_strings_are_rejected() {
138 for (url, cookie, html, field) in [
139 ("", "C", "H", "websiteURL"),
140 ("https://example.com", "", "H", "metadata.tspdCookie"),
141 ("https://example.com", "C", "", "metadata.htmlPageBase64"),
142 ] {
143 let err = TspdCustomTaskRequest::new(url, TspdMetadata::new(cookie, html), proxy())
144 .to_task()
145 .unwrap_err();
146 assert!(matches!(err, Error::Validation(ref m) if m.contains(field)));
147 }
148 }
149}