Skip to main content

capmonstercloud_client/requests/
recaptcha_complex_image.rs

1use 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/// Image grid size of a reCAPTCHA challenge (`Grid` inside `metadata`).
9#[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/// `metadata` of the reCAPTCHA click task.
20///
21/// The docs require `Grid` and at least one of `Task` / `TaskDefinition`.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23pub struct RecaptchaMetadata {
24    /// Image grid size.
25    #[serde(rename = "Grid")]
26    pub grid: RecaptchaGrid,
27    /// Task name in English, e.g. `Click on traffic lights`.
28    #[serde(rename = "Task", skip_serializing_if = "Option::is_none")]
29    pub task: Option<String>,
30    /// Technical task identifier, e.g. `/m/015qff` (taken from the `/recaptcha/<api>/reload` or
31    /// `/userverify` response).
32    #[serde(rename = "TaskDefinition", skip_serializing_if = "Option::is_none")]
33    pub task_definition: Option<String>,
34}
35
36impl RecaptchaMetadata {
37    /// Metadata identified by the task name.
38    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    /// Metadata identified by the technical `TaskDefinition` value.
47    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/// reCAPTCHA v2 image click task (`ComplexImageTask`, class `recaptcha`). No proxy is needed.
67///
68/// Pass the full 4x4 / 3x3 image or a new 1x1 tile either by URL or as base64 (one entry max).
69#[derive(Debug, Clone, Serialize)]
70pub struct RecaptchaComplexImageTaskRequest {
71    /// Image links (array of one); required if `images_base64` is not set.
72    #[serde(rename = "imageUrls", skip_serializing_if = "Option::is_none")]
73    pub image_urls: Option<Vec<String>>,
74    /// Base64 images without the `data:image/jpeg;base64,` prefix (array of one); required if
75    /// `image_urls` is not set.
76    #[serde(rename = "imagesBase64", skip_serializing_if = "Option::is_none")]
77    pub images_base64: Option<Vec<String>>,
78    /// Grid and task description.
79    pub metadata: RecaptchaMetadata,
80    /// User-Agent to use when loading images passed as URLs.
81    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
82    pub user_agent: Option<String>,
83    /// URL of the page where the captcha is solved.
84    #[serde(rename = "websiteURL", skip_serializing_if = "Option::is_none")]
85    pub website_url: Option<String>,
86}
87
88impl RecaptchaComplexImageTaskRequest {
89    /// Request with the image given by URL.
90    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    /// Request with the image given as base64.
101    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}