capmonstercloud_client/requests/
recaptcha_complex_image.rs1use serde::Serialize;
2
3use super::complex_image::{check_image_list, require_images, timeouts};
4use super::CaptchaRequest;
5use crate::error::{Error, Result};
6use crate::timeouts::GetResultTimeouts;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10pub enum RecaptchaGrid {
11 #[serde(rename = "3x3")]
12 ThreeByThree,
13 #[serde(rename = "4x4")]
14 FourByFour,
15 #[serde(rename = "1x1")]
16 OneByOne,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23pub struct RecaptchaMetadata {
24 #[serde(rename = "Grid")]
26 pub grid: RecaptchaGrid,
27 #[serde(rename = "Task", skip_serializing_if = "Option::is_none")]
29 pub task: Option<String>,
30 #[serde(rename = "TaskDefinition", skip_serializing_if = "Option::is_none")]
33 pub task_definition: Option<String>,
34}
35
36impl RecaptchaMetadata {
37 pub fn from_task(grid: RecaptchaGrid, task: impl Into<String>) -> Self {
39 Self {
40 grid,
41 task: Some(task.into()),
42 task_definition: None,
43 }
44 }
45
46 pub fn from_task_definition(grid: RecaptchaGrid, task_definition: impl Into<String>) -> Self {
48 Self {
49 grid,
50 task: None,
51 task_definition: Some(task_definition.into()),
52 }
53 }
54
55 pub fn with_task(mut self, value: impl Into<String>) -> Self {
56 self.task = Some(value.into());
57 self
58 }
59
60 pub fn with_task_definition(mut self, value: impl Into<String>) -> Self {
61 self.task_definition = Some(value.into());
62 self
63 }
64}
65
66#[derive(Debug, Clone, Serialize)]
70pub struct RecaptchaComplexImageTaskRequest {
71 #[serde(rename = "imageUrls", skip_serializing_if = "Option::is_none")]
73 pub image_urls: Option<Vec<String>>,
74 #[serde(rename = "imagesBase64", skip_serializing_if = "Option::is_none")]
77 pub images_base64: Option<Vec<String>>,
78 pub metadata: RecaptchaMetadata,
80 #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
82 pub user_agent: Option<String>,
83 #[serde(rename = "websiteURL", skip_serializing_if = "Option::is_none")]
85 pub website_url: Option<String>,
86}
87
88impl RecaptchaComplexImageTaskRequest {
89 pub fn from_url(image_url: impl Into<String>, metadata: RecaptchaMetadata) -> Self {
91 Self {
92 image_urls: Some(vec![image_url.into()]),
93 images_base64: None,
94 metadata,
95 user_agent: None,
96 website_url: None,
97 }
98 }
99
100 pub fn from_base64(image_base64: impl Into<String>, metadata: RecaptchaMetadata) -> Self {
102 Self {
103 image_urls: None,
104 images_base64: Some(vec![image_base64.into()]),
105 metadata,
106 user_agent: None,
107 website_url: None,
108 }
109 }
110
111 pub fn with_image_urls(mut self, value: Vec<String>) -> Self {
112 self.image_urls = Some(value);
113 self
114 }
115
116 pub fn with_images_base64(mut self, value: Vec<String>) -> Self {
117 self.images_base64 = Some(value);
118 self
119 }
120
121 pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
122 self.user_agent = Some(value.into());
123 self
124 }
125
126 pub fn with_website_url(mut self, value: impl Into<String>) -> Self {
127 self.website_url = Some(value.into());
128 self
129 }
130}
131
132impl CaptchaRequest for RecaptchaComplexImageTaskRequest {
133 const TASK_TYPE: &'static str = "ComplexImageTask";
134 const CLASS: Option<&'static str> = Some("recaptcha");
135
136 fn timeouts(&self) -> GetResultTimeouts {
137 timeouts()
138 }
139
140 fn validate(&self) -> Result<()> {
141 if self.metadata.task.is_none() && self.metadata.task_definition.is_none() {
142 return Err(Error::Validation(
143 "metadata: either Task or TaskDefinition must be set".into(),
144 ));
145 }
146 require_images(self.image_urls.as_ref(), self.images_base64.as_ref())?;
147 check_image_list("image_urls", self.image_urls.as_ref(), 1)?;
148 check_image_list("images_base64", self.images_base64.as_ref(), 1)
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use serde_json::json;
156
157 fn meta() -> RecaptchaMetadata {
158 RecaptchaMetadata::from_task(RecaptchaGrid::ThreeByThree, "Click on traffic lights")
159 }
160
161 #[test]
162 fn minimal_task() {
163 let task = RecaptchaComplexImageTaskRequest::from_base64("AAA", meta())
164 .to_task()
165 .unwrap();
166 assert_eq!(
167 task,
168 json!({"type": "ComplexImageTask", "class": "recaptcha", "imagesBase64": ["AAA"],
169 "metadata": {"Grid": "3x3", "Task": "Click on traffic lights"}})
170 );
171 }
172
173 #[test]
174 fn full_task() {
175 let metadata =
176 RecaptchaMetadata::from_task_definition(RecaptchaGrid::FourByFour, "/m/015qff")
177 .with_task("Click on traffic lights");
178 let task = RecaptchaComplexImageTaskRequest::from_url("https://i/x.jpg", metadata)
179 .with_images_base64(vec!["B".into()])
180 .with_user_agent("UA")
181 .with_website_url("https://example.com")
182 .to_task()
183 .unwrap();
184 assert_eq!(
185 task,
186 json!({"type": "ComplexImageTask", "class": "recaptcha",
187 "imageUrls": ["https://i/x.jpg"], "imagesBase64": ["B"],
188 "metadata": {"Grid": "4x4", "Task": "Click on traffic lights",
189 "TaskDefinition": "/m/015qff"},
190 "userAgent": "UA", "websiteURL": "https://example.com"})
191 );
192 }
193
194 #[test]
195 fn grid_serialization() {
196 assert_eq!(
197 serde_json::to_value(RecaptchaGrid::OneByOne).unwrap(),
198 json!("1x1")
199 );
200 }
201
202 #[test]
203 fn metadata_needs_task_or_definition() {
204 let mut m = meta();
205 m.task = None;
206 let err = RecaptchaComplexImageTaskRequest::from_base64("A", m)
207 .to_task()
208 .unwrap_err();
209 assert!(matches!(err, Error::Validation(_)));
210 }
211
212 #[test]
213 fn image_checks() {
214 let mut r = RecaptchaComplexImageTaskRequest::from_base64("A", meta());
215 r.images_base64 = None;
216 assert!(r.to_task().is_err(), "no images");
217 let r = RecaptchaComplexImageTaskRequest::from_base64("A", meta())
218 .with_images_base64(vec!["A".into(), "B".into()]);
219 assert!(r.to_task().is_err(), "more than one image");
220 let r = RecaptchaComplexImageTaskRequest::from_base64("A", meta()).with_image_urls(vec![]);
221 assert!(r.to_task().is_err(), "empty list");
222 }
223}