capmonstercloud_client/
client.rs1use std::time::Duration;
4
5use serde::Deserialize;
6use serde_json::{json, Value};
7use tokio::time::{sleep, Instant};
8
9use crate::error::{Error, Result};
10use crate::proxy::{ClientProxy, ProxyType};
11use crate::requests::CaptchaRequest;
12use crate::timeouts::GetResultTimeouts;
13
14pub const DEFAULT_SERVICE_URL: &str = "https://api.capmonster.cloud";
16pub const DEFAULT_SOFT_ID: u32 = 152;
18
19#[derive(Clone)]
21pub struct ClientOptions {
22 pub api_key: String,
23 pub service_url: String,
24 pub soft_id: u32,
25 pub client_timeout: Duration,
27 pub client_proxy: Option<ClientProxy>,
29 pub timeouts_override: Option<GetResultTimeouts>,
31}
32
33impl ClientOptions {
34 pub fn new(api_key: impl Into<String>) -> Self {
35 Self {
36 api_key: api_key.into(),
37 service_url: DEFAULT_SERVICE_URL.to_string(),
38 soft_id: DEFAULT_SOFT_ID,
39 client_timeout: Duration::from_secs(20),
40 client_proxy: None,
41 timeouts_override: None,
42 }
43 }
44
45 pub fn with_service_url(mut self, url: impl Into<String>) -> Self {
46 self.service_url = url.into();
47 self
48 }
49
50 pub fn with_soft_id(mut self, soft_id: u32) -> Self {
51 self.soft_id = soft_id;
52 self
53 }
54
55 pub fn with_client_timeout(mut self, timeout: Duration) -> Self {
56 self.client_timeout = timeout;
57 self
58 }
59
60 pub fn with_client_proxy(mut self, proxy: ClientProxy) -> Self {
61 self.client_proxy = Some(proxy);
62 self
63 }
64
65 pub fn with_timeouts_override(mut self, timeouts: GetResultTimeouts) -> Self {
66 self.timeouts_override = Some(timeouts);
67 self
68 }
69}
70
71impl std::fmt::Debug for ClientOptions {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("ClientOptions")
75 .field("api_key", &"***")
76 .field("service_url", &self.service_url)
77 .field("soft_id", &self.soft_id)
78 .field("client_timeout", &self.client_timeout)
79 .field("client_proxy", &self.client_proxy)
80 .field("timeouts_override", &self.timeouts_override)
81 .finish()
82 }
83}
84
85#[derive(Debug, Deserialize)]
88#[serde(rename_all = "camelCase")]
89struct ApiResponse {
90 error_id: i64,
91 error_code: Option<String>,
92 error_description: Option<String>,
93 task_id: Option<u64>,
94 status: Option<String>,
95 solution: Option<Value>,
96 balance: Option<f64>,
97}
98
99impl ApiResponse {
100 fn code(&self) -> String {
101 self.error_code.clone().unwrap_or_default()
102 }
103 fn description(&self) -> String {
104 self.error_description.clone().unwrap_or_default()
105 }
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub enum TaskStatus {
111 Processing,
112 Ready(Value),
114}
115
116#[derive(Debug, Clone)]
118pub struct CapMonsterClient {
119 options: ClientOptions,
120 http: reqwest::Client,
121}
122
123impl CapMonsterClient {
124 pub fn new(options: ClientOptions) -> Result<Self> {
125 let user_agent = format!(
126 "Zennolab.CapMonsterCloud.Client.Rust/{}",
127 env!("CARGO_PKG_VERSION")
128 );
129 let mut builder = reqwest::Client::builder()
130 .user_agent(user_agent)
131 .timeout(options.client_timeout);
132
133 if let Some(p) = &options.client_proxy {
134 if !matches!(p.proxy_type, ProxyType::Http | ProxyType::Https) {
135 return Err(Error::UnsupportedProxyType(
136 p.proxy_type.as_str().to_string(),
137 ));
138 }
139 let url = format!("{}://{}:{}", p.proxy_type.as_str(), p.address, p.port);
140 let mut proxy = reqwest::Proxy::all(url)?;
141 if let (Some(login), Some(password)) = (&p.login, &p.password) {
142 proxy = proxy.basic_auth(login, password);
143 }
144 builder = builder.proxy(proxy);
145 }
146
147 Ok(Self {
148 options,
149 http: builder.build()?,
150 })
151 }
152
153 pub fn options(&self) -> &ClientOptions {
154 &self.options
155 }
156
157 pub async fn get_balance(&self) -> Result<f64> {
159 let resp = self
160 .post("getBalance", json!({ "clientKey": self.options.api_key }))
161 .await?;
162 if resp.error_id != 0 {
163 return Err(Error::GetBalance {
164 code: resp.code(),
165 description: resp.description(),
166 });
167 }
168 resp.balance
169 .ok_or_else(|| Error::MalformedResponse("getBalance: no `balance` field".into()))
170 }
171
172 pub async fn create_task<R: CaptchaRequest>(&self, request: &R) -> Result<u64> {
174 let body = json!({
175 "clientKey": self.options.api_key,
176 "task": request.to_task()?,
177 "softId": self.options.soft_id,
178 });
179 let resp = self.post("createTask", body).await?;
180 if resp.error_id != 0 {
181 return Err(Error::CreateTask {
182 code: resp.code(),
183 description: resp.description(),
184 });
185 }
186 resp.task_id
187 .ok_or_else(|| Error::MalformedResponse("createTask: no `taskId` field".into()))
188 }
189
190 pub async fn get_task_result(&self, task_id: u64) -> Result<TaskStatus> {
192 let body = json!({ "clientKey": self.options.api_key, "taskId": task_id });
193 let resp = match self.post("getTaskResult", body).await {
194 Ok(r) => r,
195 Err(Error::Status { status: 500, .. }) => return Ok(TaskStatus::Processing),
197 Err(e) => return Err(e),
198 };
199 if resp.error_id != 0 {
200 return Err(Error::GetResult {
201 code: resp.code(),
202 description: resp.description(),
203 });
204 }
205 match resp.status.as_deref() {
206 Some("ready") => Ok(TaskStatus::Ready(resp.solution.unwrap_or(Value::Null))),
207 _ => Ok(TaskStatus::Processing),
208 }
209 }
210
211 pub async fn solve_captcha<R: CaptchaRequest>(&self, request: &R) -> Result<Value> {
215 let timeouts = self
216 .options
217 .timeouts_override
218 .unwrap_or_else(|| request.timeouts());
219 let task_id = self.create_task(request).await?;
220
221 let now = Instant::now();
222 let deadline = now
224 .checked_add(timeouts.timeout)
225 .unwrap_or_else(|| now + Duration::from_secs(365 * 24 * 3600));
226 sleep(timeouts.first_request_delay).await;
227 while Instant::now() < deadline {
228 if let TaskStatus::Ready(solution) = self.get_task_result(task_id).await? {
229 return Ok(solution);
230 }
231 sleep(timeouts.requests_interval).await;
232 }
233 Err(Error::Timeout(timeouts.timeout))
234 }
235
236 async fn post(&self, method: &'static str, body: Value) -> Result<ApiResponse> {
237 let url = format!(
238 "{}/{}",
239 self.options.service_url.trim_end_matches('/'),
240 method
241 );
242 let resp = self.http.post(url).json(&body).send().await?;
243 let status = resp.status();
244 if !status.is_success() {
245 return Err(Error::Status {
246 method,
247 status: status.as_u16(),
248 });
249 }
250 let text = resp.text().await?;
252 Ok(serde_json::from_str(&text)?)
253 }
254}