trustcaptcha 3.0.0

TrustCaptcha – Privacy-first CAPTCHA solution for Rust. GDPR-compliant, bot protection made in Europe.
Documentation
use crate::errors::{
    ApiKeyInvalidError, ClientReportedServerUnreachableError, ServerUnreachableError, UnknownError,
    VerificationNotFinishedError, VerificationNotFoundError, VerificationResultExpiredError,
    VerificationResultRetrievalLimitReachedError, VerificationTokenInvalidError,
};
use crate::model::verification_result::VerificationResult;
use crate::model::verification_token::VerificationToken;
use base64::{engine::general_purpose, Engine as _};
use reqwest::{redirect::Policy, Client, Proxy};
use serde_json::json;
use std::error::Error;
use std::time::Duration;

const LIBRARY_VERSION: &str = "3.0.0";
const LIBRARY_LANGUAGE: &str = "rust";
const DEFAULT_API_HOST: &str = "https://api.trustcomponent.com";
const DEFAULT_CONNECT_TIMEOUT_S: u64 = 3;
const DEFAULT_READ_TIMEOUT_S: u64 = 5;

pub struct TrustCaptcha {
    api_key: String,
    api_host: String,
    connect_timeout: Duration,
    read_timeout: Duration,
    proxy: Option<String>,
}

pub struct TrustCaptchaBuilder {
    api_key: String,
    api_host: String,
    connect_timeout: Duration,
    read_timeout: Duration,
    proxy: Option<String>,
}

impl TrustCaptchaBuilder {
    pub fn api_host(mut self, api_host: impl Into<String>) -> Self {
        self.api_host = api_host.into();
        self
    }

    pub fn connect_timeout(mut self, d: Duration) -> Self {
        self.connect_timeout = d;
        self
    }

    pub fn read_timeout(mut self, d: Duration) -> Self {
        self.read_timeout = d;
        self
    }

    pub fn proxy(mut self, proxy_url: impl Into<String>) -> Self {
        self.proxy = Some(proxy_url.into());
        self
    }

    pub fn build(self) -> Result<TrustCaptcha, Box<dyn Error>> {
        let api_key = self.api_key.trim().to_string();
        if api_key.is_empty() {
            return Err("api_key must not be empty".into());
        }
        Ok(TrustCaptcha {
            api_key,
            api_host: self.api_host,
            connect_timeout: self.connect_timeout,
            read_timeout: self.read_timeout,
            proxy: self.proxy,
        })
    }
}

impl TrustCaptcha {
    pub fn builder(api_key: impl Into<String>) -> TrustCaptchaBuilder {
        TrustCaptchaBuilder {
            api_key: api_key.into(),
            api_host: DEFAULT_API_HOST.to_string(),
            connect_timeout: Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_S),
            read_timeout: Duration::from_secs(DEFAULT_READ_TIMEOUT_S),
            proxy: None,
        }
    }

    pub async fn get_verification_result(
        &self,
        base64_verification_token: &str,
    ) -> Result<VerificationResult, Box<dyn Error>> {
        let verification_token = match Self::parse_verification_token(base64_verification_token) {
            Ok(t) => t,
            Err(_) => return Err(Box::new(VerificationTokenInvalidError)),
        };

        let url = if verification_token.client_failover {
            format!(
                "{}/v2/verifications/{}/results?clientFailover=true",
                self.api_host, verification_token.verification_id
            )
        } else {
            format!(
                "{}/v2/verifications/{}/results",
                self.api_host, verification_token.verification_id
            )
        };

        let mut builder = Client::builder()
            .redirect(Policy::none())
            .connect_timeout(self.connect_timeout)
            .timeout(self.read_timeout);

        if let Some(proxy_url) = &self.proxy {
            builder = builder.proxy(Proxy::all(proxy_url)?);
        }

        let client = builder.build()?;

        let response = match client
            .get(&url)
            .header("Authorization", format!("Bearer {}", self.api_key))
            .header("User-Agent", build_user_agent())
            .send()
            .await
        {
            Ok(r) => r,
            Err(e) if e.is_connect() || e.is_timeout() || e.is_request() => {
                return Err(Box::new(ServerUnreachableError));
            }
            Err(e) => return Err(Box::new(e)),
        };

        match response.status().as_u16() {
            200 => Ok(response.json::<VerificationResult>().await?),
            403 => Err(Box::new(ApiKeyInvalidError)),
            404 => Err(Box::new(VerificationNotFoundError)),
            410 => Err(Box::new(VerificationResultExpiredError)),
            412 => Err(Box::new(ClientReportedServerUnreachableError)),
            423 => Err(Box::new(VerificationNotFinishedError)),
            429 => Err(Box::new(VerificationResultRetrievalLimitReachedError)),
            _ => Err(Box::new(UnknownError)),
        }
    }

    fn parse_verification_token(
        base64_verification_token: &str,
    ) -> Result<VerificationToken, Box<dyn Error>> {
        let decoded = general_purpose::STANDARD.decode(base64_verification_token)?;
        let s = String::from_utf8(decoded)?;
        let token: VerificationToken = serde_json::from_str(&s)?;
        Ok(token)
    }
}

fn build_user_agent() -> String {
    let payload = json!({
        "language": LIBRARY_LANGUAGE,
        "version": LIBRARY_VERSION,
    });
    let encoded = general_purpose::STANDARD.encode(payload.to_string().as_bytes());
    format!("Trustcaptcha/{}", encoded)
}