capmonstercloud_client/requests/
hunt.rs1use serde::ser::{Serialize, SerializeMap, Serializer};
2use serde::Serialize as DeriveSerialize;
3
4use super::{require_non_empty, CaptchaRequest};
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum HuntMode {
11 Fingerprint,
13 Token(String),
15 WidgetUrl(String),
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct HuntMetadata {
23 pub api_get_lib: String,
26 pub mode: HuntMode,
27}
28
29impl HuntMetadata {
30 pub fn fingerprint(api_get_lib: impl Into<String>) -> Self {
32 Self {
33 api_get_lib: api_get_lib.into(),
34 mode: HuntMode::Fingerprint,
35 }
36 }
37
38 pub fn with_token(api_get_lib: impl Into<String>, data: impl Into<String>) -> Self {
40 Self {
41 api_get_lib: api_get_lib.into(),
42 mode: HuntMode::Token(data.into()),
43 }
44 }
45
46 pub fn with_widget_url(api_get_lib: impl Into<String>, widget_url: impl Into<String>) -> Self {
48 Self {
49 api_get_lib: api_get_lib.into(),
50 mode: HuntMode::WidgetUrl(widget_url.into()),
51 }
52 }
53}
54
55impl Serialize for HuntMetadata {
56 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
57 let len = if self.mode == HuntMode::Fingerprint {
58 1
59 } else {
60 2
61 };
62 let mut map = serializer.serialize_map(Some(len))?;
63 map.serialize_entry("apiGetLib", &self.api_get_lib)?;
64 match &self.mode {
65 HuntMode::Fingerprint => {}
66 HuntMode::Token(data) => map.serialize_entry("data", data)?,
67 HuntMode::WidgetUrl(url) => map.serialize_entry("widgetUrl", url)?,
68 }
69 map.end()
70 }
71}
72
73#[derive(Debug, Clone, DeriveSerialize)]
75pub struct HuntCustomTaskRequest {
76 #[serde(rename = "websiteURL")]
78 pub website_url: String,
79 pub metadata: HuntMetadata,
80 #[serde(flatten)]
82 pub proxy: ProxyInfo,
83 #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
85 pub user_agent: Option<String>,
86}
87
88impl HuntCustomTaskRequest {
89 pub fn new(website_url: impl Into<String>, metadata: HuntMetadata, proxy: ProxyInfo) -> Self {
90 Self {
91 website_url: website_url.into(),
92 metadata,
93 proxy,
94 user_agent: None,
95 }
96 }
97
98 pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
99 self.user_agent = Some(value.into());
100 self
101 }
102}
103
104impl CaptchaRequest for HuntCustomTaskRequest {
105 const TASK_TYPE: &'static str = "CustomTask";
106 const CLASS: Option<&'static str> = Some("HUNT");
107
108 fn timeouts(&self) -> GetResultTimeouts {
109 GetResultTimeouts::secs(1.0, 0.0, 3.0, 180.0)
110 }
111
112 fn validate(&self) -> crate::error::Result<()> {
113 require_non_empty("websiteURL", &self.website_url)?;
114 require_non_empty("metadata.apiGetLib", &self.metadata.api_get_lib)?;
115 match &self.metadata.mode {
116 HuntMode::Fingerprint => {}
117 HuntMode::Token(data) => require_non_empty("metadata.data", data)?,
118 HuntMode::WidgetUrl(url) => require_non_empty("metadata.widgetUrl", url)?,
119 }
120 Ok(())
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::proxy::ProxyType;
128 use serde_json::json;
129
130 fn proxy() -> ProxyInfo {
131 ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080).with_auth("u", "p")
132 }
133
134 #[test]
135 fn minimal_fingerprint_task() {
136 let task = HuntCustomTaskRequest::new(
137 "https://example.com",
138 HuntMetadata::fingerprint("https://example.com/api.js"),
139 proxy(),
140 )
141 .to_task()
142 .unwrap();
143 assert_eq!(
144 task,
145 json!({
146 "type": "CustomTask", "class": "HUNT", "websiteURL": "https://example.com",
147 "metadata": {"apiGetLib": "https://example.com/api.js"},
148 "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
149 "proxyLogin": "u", "proxyPassword": "p"
150 })
151 );
152 }
153
154 #[test]
155 fn full_token_task() {
156 let task = HuntCustomTaskRequest::new(
157 "https://example.com",
158 HuntMetadata::with_token("L", "TOKEN"),
159 proxy(),
160 )
161 .with_user_agent("UA")
162 .to_task()
163 .unwrap();
164 assert_eq!(task["metadata"], json!({"apiGetLib": "L", "data": "TOKEN"}));
165 assert_eq!(task["userAgent"], "UA");
166 }
167
168 #[test]
169 fn widget_url_task() {
170 let task = HuntCustomTaskRequest::new(
171 "https://example.com",
172 HuntMetadata::with_widget_url("L", "https://c.example.com/widget?hash=h"),
173 proxy(),
174 )
175 .to_task()
176 .unwrap();
177 assert_eq!(
178 task["metadata"],
179 json!({"apiGetLib": "L", "widgetUrl": "https://c.example.com/widget?hash=h"})
180 );
181 }
182
183 #[test]
184 fn empty_required_strings_are_rejected() {
185 let err = HuntCustomTaskRequest::new("", HuntMetadata::fingerprint("L"), proxy())
186 .to_task()
187 .unwrap_err();
188 assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
189 let err = HuntCustomTaskRequest::new(
190 "https://example.com",
191 HuntMetadata::fingerprint(""),
192 proxy(),
193 )
194 .to_task()
195 .unwrap_err();
196 assert!(
197 matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.apiGetLib"))
198 );
199 let err = HuntCustomTaskRequest::new(
200 "https://example.com",
201 HuntMetadata::with_token("L", ""),
202 proxy(),
203 )
204 .to_task()
205 .unwrap_err();
206 assert!(
207 matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.data"))
208 );
209 let err = HuntCustomTaskRequest::new(
210 "https://example.com",
211 HuntMetadata::with_widget_url("L", ""),
212 proxy(),
213 )
214 .to_task()
215 .unwrap_err();
216 assert!(
217 matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.widgetUrl"))
218 );
219 }
220}