use serde::Serialize;
use super::complex_image::{check_image_list, require_images, timeouts};
use super::CaptchaRequest;
use crate::error::{Error, Result};
use crate::timeouts::GetResultTimeouts;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum RecaptchaGrid {
#[serde(rename = "3x3")]
ThreeByThree,
#[serde(rename = "4x4")]
FourByFour,
#[serde(rename = "1x1")]
OneByOne,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RecaptchaMetadata {
#[serde(rename = "Grid")]
pub grid: RecaptchaGrid,
#[serde(rename = "Task", skip_serializing_if = "Option::is_none")]
pub task: Option<String>,
#[serde(rename = "TaskDefinition", skip_serializing_if = "Option::is_none")]
pub task_definition: Option<String>,
}
impl RecaptchaMetadata {
pub fn from_task(grid: RecaptchaGrid, task: impl Into<String>) -> Self {
Self {
grid,
task: Some(task.into()),
task_definition: None,
}
}
pub fn from_task_definition(grid: RecaptchaGrid, task_definition: impl Into<String>) -> Self {
Self {
grid,
task: None,
task_definition: Some(task_definition.into()),
}
}
pub fn with_task(mut self, value: impl Into<String>) -> Self {
self.task = Some(value.into());
self
}
pub fn with_task_definition(mut self, value: impl Into<String>) -> Self {
self.task_definition = Some(value.into());
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct RecaptchaComplexImageTaskRequest {
#[serde(rename = "imageUrls", skip_serializing_if = "Option::is_none")]
pub image_urls: Option<Vec<String>>,
#[serde(rename = "imagesBase64", skip_serializing_if = "Option::is_none")]
pub images_base64: Option<Vec<String>>,
pub metadata: RecaptchaMetadata,
#[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>,
#[serde(rename = "websiteURL", skip_serializing_if = "Option::is_none")]
pub website_url: Option<String>,
}
impl RecaptchaComplexImageTaskRequest {
pub fn from_url(image_url: impl Into<String>, metadata: RecaptchaMetadata) -> Self {
Self {
image_urls: Some(vec![image_url.into()]),
images_base64: None,
metadata,
user_agent: None,
website_url: None,
}
}
pub fn from_base64(image_base64: impl Into<String>, metadata: RecaptchaMetadata) -> Self {
Self {
image_urls: None,
images_base64: Some(vec![image_base64.into()]),
metadata,
user_agent: None,
website_url: None,
}
}
pub fn with_image_urls(mut self, value: Vec<String>) -> Self {
self.image_urls = Some(value);
self
}
pub fn with_images_base64(mut self, value: Vec<String>) -> Self {
self.images_base64 = Some(value);
self
}
pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
self.user_agent = Some(value.into());
self
}
pub fn with_website_url(mut self, value: impl Into<String>) -> Self {
self.website_url = Some(value.into());
self
}
}
impl CaptchaRequest for RecaptchaComplexImageTaskRequest {
const TASK_TYPE: &'static str = "ComplexImageTask";
const CLASS: Option<&'static str> = Some("recaptcha");
fn timeouts(&self) -> GetResultTimeouts {
timeouts()
}
fn validate(&self) -> Result<()> {
if self.metadata.task.is_none() && self.metadata.task_definition.is_none() {
return Err(Error::Validation(
"metadata: either Task or TaskDefinition must be set".into(),
));
}
require_images(self.image_urls.as_ref(), self.images_base64.as_ref())?;
check_image_list("image_urls", self.image_urls.as_ref(), 1)?;
check_image_list("images_base64", self.images_base64.as_ref(), 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn meta() -> RecaptchaMetadata {
RecaptchaMetadata::from_task(RecaptchaGrid::ThreeByThree, "Click on traffic lights")
}
#[test]
fn minimal_task() {
let task = RecaptchaComplexImageTaskRequest::from_base64("AAA", meta())
.to_task()
.unwrap();
assert_eq!(
task,
json!({"type": "ComplexImageTask", "class": "recaptcha", "imagesBase64": ["AAA"],
"metadata": {"Grid": "3x3", "Task": "Click on traffic lights"}})
);
}
#[test]
fn full_task() {
let metadata =
RecaptchaMetadata::from_task_definition(RecaptchaGrid::FourByFour, "/m/015qff")
.with_task("Click on traffic lights");
let task = RecaptchaComplexImageTaskRequest::from_url("https://i/x.jpg", metadata)
.with_images_base64(vec!["B".into()])
.with_user_agent("UA")
.with_website_url("https://example.com")
.to_task()
.unwrap();
assert_eq!(
task,
json!({"type": "ComplexImageTask", "class": "recaptcha",
"imageUrls": ["https://i/x.jpg"], "imagesBase64": ["B"],
"metadata": {"Grid": "4x4", "Task": "Click on traffic lights",
"TaskDefinition": "/m/015qff"},
"userAgent": "UA", "websiteURL": "https://example.com"})
);
}
#[test]
fn grid_serialization() {
assert_eq!(
serde_json::to_value(RecaptchaGrid::OneByOne).unwrap(),
json!("1x1")
);
}
#[test]
fn metadata_needs_task_or_definition() {
let mut m = meta();
m.task = None;
let err = RecaptchaComplexImageTaskRequest::from_base64("A", m)
.to_task()
.unwrap_err();
assert!(matches!(err, Error::Validation(_)));
}
#[test]
fn image_checks() {
let mut r = RecaptchaComplexImageTaskRequest::from_base64("A", meta());
r.images_base64 = None;
assert!(r.to_task().is_err(), "no images");
let r = RecaptchaComplexImageTaskRequest::from_base64("A", meta())
.with_images_base64(vec!["A".into(), "B".into()]);
assert!(r.to_task().is_err(), "more than one image");
let r = RecaptchaComplexImageTaskRequest::from_base64("A", meta()).with_image_urls(vec![]);
assert!(r.to_task().is_err(), "empty list");
}
}