use base64::{engine::general_purpose::STANDARD, Engine as _};
use serde::{Serialize, Serializer};
use super::CaptchaRequest;
use crate::error::{Error, Result};
use crate::timeouts::GetResultTimeouts;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TextModule {
Amazon,
Apple,
Botdetect,
Facebook,
Gmx,
Google,
Hotmail,
Mailru,
Okeng,
Okrus,
Partiallyblur,
Ramblerrus,
Ramblerrusnew,
Solvemedia,
Steam,
Vk,
Whatsapp,
WikimediaPaddleEnsemble,
Yandex,
Yandexnew,
Yandexwave,
Yandexwavelatin,
CaptchaMath,
Sat,
BlsText,
Universal,
Custom(String),
}
impl TextModule {
pub fn as_str(&self) -> &str {
match self {
TextModule::Amazon => "amazon",
TextModule::Apple => "apple",
TextModule::Botdetect => "botdetect",
TextModule::Facebook => "facebook",
TextModule::Gmx => "gmx",
TextModule::Google => "google",
TextModule::Hotmail => "hotmail",
TextModule::Mailru => "mailru",
TextModule::Okeng => "okeng",
TextModule::Okrus => "okrus",
TextModule::Partiallyblur => "partiallyblur",
TextModule::Ramblerrus => "ramblerrus",
TextModule::Ramblerrusnew => "ramblerrusnew",
TextModule::Solvemedia => "solvemedia",
TextModule::Steam => "steam",
TextModule::Vk => "vk",
TextModule::Whatsapp => "whatsapp",
TextModule::WikimediaPaddleEnsemble => "wikimedia_paddle_ensemble",
TextModule::Yandex => "yandex",
TextModule::Yandexnew => "yandexnew",
TextModule::Yandexwave => "yandexwave",
TextModule::Yandexwavelatin => "yandexwavelatin",
TextModule::CaptchaMath => "captcha_math",
TextModule::Sat => "sat",
TextModule::BlsText => "bls_text",
TextModule::Universal => "universal",
TextModule::Custom(name) => name,
}
}
}
impl Serialize for TextModule {
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
fn as_base64<S: Serializer>(bytes: &[u8], s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&STANDARD.encode(bytes))
}
#[derive(Debug, Clone, Serialize)]
pub struct ImageToTextRequest {
#[serde(rename = "body", serialize_with = "as_base64")]
pub image_bytes: Vec<u8>,
#[serde(rename = "capMonsterModule", skip_serializing_if = "Option::is_none")]
pub module_name: Option<TextModule>,
#[serde(
rename = "recognizingThreshold",
skip_serializing_if = "Option::is_none"
)]
pub threshold: Option<u8>,
#[serde(rename = "case", skip_serializing_if = "Option::is_none")]
pub case: Option<bool>,
#[serde(
serialize_with = "bool_as_int",
skip_serializing_if = "Option::is_none"
)]
pub numeric: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub math: Option<bool>,
}
fn bool_as_int<S: Serializer>(v: &Option<bool>, s: S) -> std::result::Result<S::Ok, S::Error> {
match v {
Some(b) => s.serialize_u8(*b as u8),
None => s.serialize_none(),
}
}
impl ImageToTextRequest {
pub fn new(image_bytes: impl Into<Vec<u8>>) -> Self {
Self {
image_bytes: image_bytes.into(),
module_name: None,
threshold: None,
case: None,
numeric: None,
math: None,
}
}
pub fn with_module_name(mut self, value: TextModule) -> Self {
self.module_name = Some(value);
self
}
pub fn with_threshold(mut self, value: u8) -> Self {
self.threshold = Some(value);
self
}
pub fn with_case(mut self, value: bool) -> Self {
self.case = Some(value);
self
}
pub fn with_numeric(mut self, value: bool) -> Self {
self.numeric = Some(value);
self
}
pub fn with_math(mut self, value: bool) -> Self {
self.math = Some(value);
self
}
}
impl CaptchaRequest for ImageToTextRequest {
const TASK_TYPE: &'static str = "ImageToTextTask";
fn timeouts(&self) -> GetResultTimeouts {
GetResultTimeouts::secs(0.35, 0.0, 0.2, 10.0)
}
fn validate(&self) -> Result<()> {
if self.image_bytes.is_empty() {
return Err(Error::Validation("body must not be empty".into()));
}
if matches!(self.threshold, Some(t) if t > 100) {
return Err(Error::Validation(
"threshold must be between 0 and 100".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn body_is_base64_and_numeric_is_int() {
let task = ImageToTextRequest::new(b"abc".to_vec())
.with_module_name(TextModule::Yandexnew)
.with_threshold(90)
.with_numeric(true)
.to_task()
.unwrap();
assert_eq!(
task,
json!({"type": "ImageToTextTask", "body": "YWJj", "capMonsterModule": "yandexnew",
"recognizingThreshold": 90, "numeric": 1})
);
}
#[test]
fn threshold_out_of_range_is_rejected() {
let err = ImageToTextRequest::new(vec![1])
.with_threshold(101)
.to_task()
.unwrap_err();
assert!(matches!(err, Error::Validation(_)));
}
#[test]
fn empty_image_bytes_are_rejected() {
let err = ImageToTextRequest::new(Vec::new()).to_task().unwrap_err();
assert!(matches!(err, Error::Validation(ref m) if m.contains("body")));
}
}