yescaptcha/
lib.rs

1use std::{
2    fmt::{self, Debug},
3    time::Duration,
4};
5
6use reqwest::IntoUrl;
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8use serde_json::json;
9use url::Url;
10
11pub mod task;
12
13pub const API_URL_INTERNATIONAL: &str = "https://api.yescaptcha.com";
14pub const API_URL_CHINA: &str = "https://cn.yescaptcha.com";
15
16pub const DEFAULT_API_URL: &str = API_URL_INTERNATIONAL;
17pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct ResponseBase {
22    pub error_id: i32,
23    pub error_code: Option<String>,
24    pub error_description: Option<String>,
25}
26
27pub trait Task: serde::Serialize + DeserializeOwned {
28    type Solution: TaskSolution + DeserializeOwned;
29
30    fn task_id(&self) -> &str;
31}
32
33pub trait TaskConfig: serde::Serialize + DeserializeOwned {
34    type Task: Task + DeserializeOwned;
35}
36
37pub trait TaskSolution: serde::Serialize + DeserializeOwned {}
38
39#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
40#[serde(rename_all = "camelCase")]
41pub enum TaskStatus {
42    Processing,
43    Ready,
44}
45
46pub enum TaskResult<T>
47where
48    T: Task,
49{
50    Processing,
51    Ready(T::Solution),
52}
53
54pub struct ClientBuilder {
55    pub reqwest_client: reqwest::Client,
56    pub api_url: Url,
57    pub client_key: Option<String>,
58}
59
60impl ClientBuilder {
61    pub fn new() -> Self {
62        Self {
63            reqwest_client: default_reqwest_builder().build().unwrap(),
64            api_url: DEFAULT_API_URL.parse().unwrap(),
65            client_key: None,
66        }
67    }
68
69    pub fn api_url(mut self, api_url: Url) -> Self {
70        self.api_url = api_url;
71        self
72    }
73
74    pub fn client_key(mut self, client_key: String) -> Self {
75        self.client_key = Some(client_key);
76        self
77    }
78
79    pub fn build(self) -> Result<Client, BuildError> {
80        if self.client_key.is_none() {
81            return Err(BuildError::InputError("client_key is required".to_string()));
82        }
83
84        Ok(Client {
85            http_client: self.reqwest_client,
86            api_url: self.api_url,
87            client_key: self.client_key.unwrap(),
88        })
89    }
90}
91
92pub struct Client {
93    pub http_client: reqwest::Client,
94    pub api_url: Url,
95    pub client_key: String,
96}
97
98impl Client {
99    pub fn new(client_key: &str) -> Self {
100        ClientBuilder::new()
101            .client_key(client_key.to_string())
102            .build()
103            .unwrap()
104    }
105
106    pub fn builder() -> ClientBuilder {
107        ClientBuilder::new()
108    }
109
110    async fn post<B: serde::Serialize, R: DeserializeOwned, U: IntoUrl>(
111        &self,
112        url: U,
113        body: B,
114    ) -> Result<R, ClientError> {
115        let body = serde_json::to_string(&body).map_err(|_| {
116            ClientError::RequestError("Failed to serialize request body".to_string())
117        })?;
118
119        let res = self
120            .http_client
121            .post(url)
122            .body(body)
123            .send()
124            .await
125            .map_err(|e| ClientError::RequestError(format!("Failed to send request: {}", e)))?;
126
127        let result = res
128            .json::<R>()
129            .await
130            .map_err(|e| ClientError::ParseError(format!("Failed to parse response: {}", e)))?;
131
132        Ok(result)
133    }
134
135    pub async fn create_task<T>(&self, task_config: T) -> Result<T::Task, ClientError>
136    where
137        T: TaskConfig,
138    {
139        let body = json!({
140            "clientKey": self.client_key.clone(),
141            "task": task_config
142        });
143
144        #[derive(Debug, Serialize, Deserialize)]
145        struct CreateTaskResponse<T: TaskConfig> {
146            #[serde(flatten)]
147            response_base: ResponseBase,
148            #[serde(flatten)]
149            task: T::Task,
150        }
151
152        let response: CreateTaskResponse<T> = self
153            .post(self.api_url.join("createTask").unwrap(), body)
154            .await?;
155
156        // Check if there was an error
157        if response.response_base.error_id != 0 {
158            return Err(ClientError::ApiError(response.response_base));
159        }
160
161        Ok(response.task)
162    }
163
164    pub async fn get_task_result<T>(&self, task: &T) -> Result<TaskResult<T>, ClientError>
165    where
166        T: Task,
167    {
168        let body = json!({
169            "clientKey": self.client_key.clone(),
170            "taskId": task.task_id()
171        });
172
173        #[derive(Debug, Serialize, Deserialize)]
174        struct GetTaskResultResponse<T: Task> {
175            #[serde(flatten)]
176            response_base: ResponseBase,
177            status: TaskStatus,
178            solution: Option<T::Solution>,
179        }
180
181        let response: GetTaskResultResponse<T> = self
182            .post(self.api_url.join("getTaskResult").unwrap(), body)
183            .await?;
184
185        // Check if there was an error
186        if response.response_base.error_id != 0 {
187            return Err(ClientError::ApiError(response.response_base));
188        }
189
190        match response.status {
191            TaskStatus::Processing => Ok(TaskResult::Processing),
192            TaskStatus::Ready => {
193                // Check if there is a solution
194                let solution = match response.solution {
195                    Some(solution) => solution,
196                    None => {
197                        return Err(ClientError::ParseError("No solution provided".to_string()))
198                    }
199                };
200
201                Ok(TaskResult::Ready(solution))
202            }
203        }
204    }
205}
206
207#[derive(Debug, Clone)]
208pub enum BuildError {
209    InputError(String),
210}
211
212impl std::error::Error for BuildError {}
213
214impl fmt::Display for BuildError {
215    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
216        match self {
217            BuildError::InputError(msg) => write!(f, "Input Error: {}", msg),
218        }
219    }
220}
221
222#[derive(Debug, Clone)]
223pub enum ClientError {
224    InputError(String),
225    RequestError(String),
226    ParseError(String),
227    ApiError(ResponseBase),
228}
229
230impl std::error::Error for ClientError {}
231
232impl fmt::Display for ClientError {
233    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
234        match self {
235            ClientError::InputError(msg) => write!(f, "Input Error: {}", msg),
236            ClientError::RequestError(msg) => write!(f, "Request Error: {}", msg),
237            ClientError::ParseError(msg) => write!(f, "Parse Error: {}", msg),
238            ClientError::ApiError(response) => write!(f, "API Error: {:?}", response),
239        }
240    }
241}
242
243pub fn default_reqwest_builder() -> reqwest::ClientBuilder {
244    reqwest::ClientBuilder::new().timeout(DEFAULT_TIMEOUT)
245}