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 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 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 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}