1use crate::error::{ApiError, ApiResponse, ApiResult, ErrorCode, LockConflictDetail};
2use crate::models::user::{RefreshTokenRequest, Token};
3use chrono::{DateTime, Duration, Utc};
4use reqwest::{Client as HttpClient, Method};
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12const API_PREFIX: &str = "/api/v4";
13pub const CR_HEADER_PREFIX: &str = "X-Cr-";
14
15#[derive(Debug, Clone)]
17pub struct ClientConfig {
18 pub base_url: String,
20 pub timeout_seconds: u64,
22 pub client_id: String,
24 pub user_agent: Option<String>,
26}
27
28impl ClientConfig {
29 pub fn new(base_url: impl Into<String>) -> Self {
31 Self {
32 base_url: base_url.into(),
33 timeout_seconds: 60,
34 client_id: "".to_string(),
35 user_agent: None,
36 }
37 }
38
39 pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
41 self.timeout_seconds = timeout_seconds;
42 self
43 }
44
45 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
47 self.client_id = client_id.into();
48 self
49 }
50
51 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
53 self.user_agent = Some(user_agent.into());
54 self
55 }
56}
57
58#[derive(Debug, Clone)]
60pub(crate) struct TokenStore {
61 access_token: Option<String>,
62 refresh_token: Option<String>,
63 access_token_expires: Option<DateTime<Utc>>,
64 refresh_token_expires: Option<DateTime<Utc>>,
65}
66
67impl TokenStore {
68 fn new() -> Self {
69 Self {
70 access_token: None,
71 refresh_token: None,
72 access_token_expires: None,
73 refresh_token_expires: None,
74 }
75 }
76
77 fn is_access_token_expired(&self) -> bool {
78 self.access_token_expires
79 .map(|exp| Utc::now() >= exp)
80 .unwrap_or(true)
81 }
82
83 fn is_refresh_token_expired(&self) -> bool {
84 self.refresh_token_expires
85 .map(|exp| Utc::now() >= exp)
86 .unwrap_or(true)
87 }
88
89 fn has_tokens(&self) -> bool {
90 self.access_token.is_some() && self.refresh_token.is_some()
91 }
92}
93
94#[derive(Debug, Clone, Default)]
96pub struct RequestOptions {
97 pub no_credential: bool,
99 pub with_purchase_ticket: bool,
101 pub skip_batch_error: bool,
103 pub skip_lock_conflict: bool,
105}
106
107impl RequestOptions {
108 pub fn new() -> Self {
109 Self::default()
110 }
111
112 pub fn no_credential(mut self) -> Self {
113 self.no_credential = true;
114 self
115 }
116
117 pub fn with_purchase_ticket(mut self) -> Self {
118 self.with_purchase_ticket = true;
119 self
120 }
121
122 pub fn skip_batch_error(mut self) -> Self {
123 self.skip_batch_error = true;
124 self
125 }
126
127 pub fn skip_lock_conflict(mut self) -> Self {
128 self.skip_lock_conflict = true;
129 self
130 }
131}
132
133pub type OnCredentialRefreshed =
135 Arc<dyn Fn(Token) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
136
137pub type OnCredentialInvalid =
139 Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
140
141pub struct Client {
143 pub(crate) config: ClientConfig,
144 pub(crate) http_client: HttpClient,
145 pub(crate) tokens: Arc<RwLock<TokenStore>>,
146 pub(crate) purchase_ticket: Arc<RwLock<Option<String>>>,
147 on_credential_refreshed: Option<OnCredentialRefreshed>,
148 on_credential_invalid: Option<OnCredentialInvalid>,
149}
150
151impl Client {
152 pub fn new(config: ClientConfig) -> Self {
154 let mut builder = HttpClient::builder()
155 .connect_timeout(std::time::Duration::from_secs(config.timeout_seconds));
156
157 if let Some(ref user_agent) = config.user_agent {
158 builder = builder.user_agent(user_agent);
159 }
160
161 let http_client = builder.build().expect("Failed to create HTTP client");
162
163 Self {
164 config,
165 http_client,
166 tokens: Arc::new(RwLock::new(TokenStore::new())),
167 purchase_ticket: Arc::new(RwLock::new(None)),
168 on_credential_refreshed: None,
169 on_credential_invalid: None,
170 }
171 }
172
173 pub fn set_on_credential_refreshed(&mut self, callback: OnCredentialRefreshed) {
192 self.on_credential_refreshed = Some(callback);
193 }
194
195 pub fn clear_on_credential_refreshed(&mut self) {
197 self.on_credential_refreshed = None;
198 }
199
200 pub fn set_on_credential_invalid(&mut self, callback: OnCredentialInvalid) {
206 self.on_credential_invalid = Some(callback);
207 }
208
209 pub fn clear_on_credential_invalid(&mut self) {
211 self.on_credential_invalid = None;
212 }
213
214 async fn notify_credential_invalid(&self) {
216 if let Some(ref callback) = self.on_credential_invalid {
217 callback().await;
218 }
219 }
220
221 pub async fn set_tokens(&self, access_token: String, refresh_token: String) {
223 let mut store = self.tokens.write().await;
224
225 let access_expires = Utc::now() + Duration::hours(1);
228 let refresh_expires = Utc::now() + Duration::days(7);
229
230 store.access_token = Some(access_token);
231 store.refresh_token = Some(refresh_token);
232 store.access_token_expires = Some(access_expires);
233 store.refresh_token_expires = Some(refresh_expires);
234 }
235
236 pub async fn set_tokens_with_expiry(&self, token: &Token) -> ApiResult<()> {
238 let mut store = self.tokens.write().await;
239
240 store.access_token = Some(token.access_token.clone());
241 store.refresh_token = Some(token.refresh_token.clone());
242
243 if let Ok(exp) = DateTime::parse_from_rfc3339(&token.access_expires) {
245 store.access_token_expires = Some(exp.with_timezone(&Utc));
246 }
247 if let Ok(exp) = DateTime::parse_from_rfc3339(&token.refresh_expires) {
248 store.refresh_token_expires = Some(exp.with_timezone(&Utc));
249 }
250
251 Ok(())
252 }
253
254 pub async fn clear_tokens(&self) {
256 let mut store = self.tokens.write().await;
257 *store = TokenStore::new();
258 }
259
260 pub async fn set_purchase_ticket(&self, ticket: Option<String>) {
262 let mut pt = self.purchase_ticket.write().await;
263 *pt = ticket;
264 }
265
266 pub(crate) async fn get_access_token(&self) -> ApiResult<String> {
268 let store = self.tokens.read().await;
269
270 if !store.has_tokens() {
272 self.notify_credential_invalid().await;
273 return Err(ApiError::NoTokensAvailable);
274 }
275
276 if store.is_refresh_token_expired() {
278 self.notify_credential_invalid().await;
279 return Err(ApiError::RefreshTokenExpired);
280 }
281
282 if !store.is_access_token_expired() {
284 return Ok(store.access_token.clone().unwrap());
285 }
286
287 drop(store); self.refresh_access_token().await
291 }
292
293 async fn refresh_access_token(&self) -> ApiResult<String> {
295 let refresh_token = {
296 let store = self.tokens.read().await;
297 store
298 .refresh_token
299 .clone()
300 .ok_or(ApiError::NoTokensAvailable)?
301 };
302
303 let url = self.build_url("/session/token/refresh");
305 let request = RefreshTokenRequest { refresh_token };
306
307 let response = self.http_client.post(&url).json(&request).send().await?;
308
309 let api_response: ApiResponse<Token> = response.json().await?;
310
311 if api_response.code != ErrorCode::Success as i32 {
312 if let Some(error_code) = ErrorCode::from_code(api_response.code) {
313 if error_code.is_credential_error() {
314 self.notify_credential_invalid().await;
315 }
316 }
317 return Err(ApiError::from_response(api_response));
318 }
319
320 let token = api_response
321 .data
322 .ok_or_else(|| ApiError::Other("No token in response".to_string()))?;
323
324 self.set_tokens_with_expiry(&token).await?;
326
327 if let Some(ref callback) = self.on_credential_refreshed {
329 callback(token.clone()).await;
330 }
331
332 Ok(token.access_token)
333 }
334
335 pub(crate) fn build_url(&self, path: &str) -> String {
337 format!("{}{}{}", self.config.base_url, API_PREFIX, path)
338 }
339
340 async fn send_internal<T, R>(
342 &self,
343 path: &str,
344 method: Method,
345 body: Option<&T>,
346 options: RequestOptions,
347 ) -> ApiResult<R>
348 where
349 T: Serialize + ?Sized,
350 R: DeserializeOwned + Default,
351 {
352 let url = self.build_url(path);
353 let mut request = self.http_client.request(method, &url);
354
355 if !options.no_credential {
357 let token = self.get_access_token().await?;
358 request = request.header("Authorization", format!("Bearer {}", token));
359 }
360
361 if !self.config.client_id.is_empty() {
363 request = request.header(
364 format!("{}Client-Id", CR_HEADER_PREFIX),
365 self.config.client_id.clone(),
366 );
367 }
368
369 if options.with_purchase_ticket {
371 let ticket = self.purchase_ticket.read().await;
372 if let Some(t) = ticket.as_ref() {
373 request = request.header(format!("{}Purchase-Ticket", CR_HEADER_PREFIX), t);
374 }
375 }
376
377 if let Some(body) = body {
379 request = request.json(body);
380 }
381
382 let response = request.send().await?;
384 let response_text = response.text().await?;
385
386 let raw_value: serde_json::Value = serde_json::from_str(&response_text)?;
388
389 let code = raw_value.get("code").and_then(|c| c.as_i64()).unwrap_or(0) as i32;
390
391 if code == ErrorCode::LockConflict as i32 {
393 let msg = raw_value
394 .get("msg")
395 .and_then(|m| m.as_str())
396 .unwrap_or("")
397 .to_string();
398 let detail: Option<LockConflictDetail> = raw_value
399 .get("data")
400 .and_then(|d| serde_json::from_value(d.clone()).ok());
401 return Err(ApiError::LockConflict {
402 message: msg,
403 detail,
404 });
405 }
406
407 let api_response: ApiResponse<R> = serde_json::from_str(&response_text)?;
409
410 if api_response.code != ErrorCode::Success as i32 {
412 if let Some(error_code) = ErrorCode::from_code(api_response.code) {
414 if error_code.is_credential_error() {
415 self.notify_credential_invalid().await;
416 }
417 }
418 return Err(ApiError::from_response(api_response));
419 }
420
421 Ok(api_response.data.unwrap_or_default())
423 }
424
425 pub async fn send<T, R>(
427 &self,
428 path: &str,
429 method: Method,
430 body: Option<&T>,
431 options: RequestOptions,
432 ) -> ApiResult<R>
433 where
434 T: Serialize + ?Sized,
435 R: DeserializeOwned + Default,
436 {
437 match self
438 .send_internal(path, method.clone(), body, options.clone())
439 .await
440 {
441 Ok(result) => Ok(result),
442 Err(ApiError::AccessTokenExpired) => {
443 self.refresh_access_token().await?;
445 self.send_internal(path, method, body, options).await
446 }
447 Err(e) => Err(e),
448 }
449 }
450
451 pub async fn get<R>(&self, path: &str, options: RequestOptions) -> ApiResult<R>
453 where
454 R: DeserializeOwned + Default,
455 {
456 self.send::<(), R>(path, Method::GET, None, options).await
457 }
458
459 pub async fn post<T, R>(&self, path: &str, body: &T, options: RequestOptions) -> ApiResult<R>
461 where
462 T: Serialize,
463 R: DeserializeOwned + Default,
464 {
465 self.send(path, Method::POST, Some(body), options).await
466 }
467
468 pub async fn put<T, R>(&self, path: &str, body: &T, options: RequestOptions) -> ApiResult<R>
470 where
471 T: Serialize,
472 R: DeserializeOwned + Default,
473 {
474 self.send(path, Method::PUT, Some(body), options).await
475 }
476
477 pub async fn delete<R>(&self, path: &str, options: RequestOptions) -> ApiResult<R>
479 where
480 R: DeserializeOwned + Default,
481 {
482 self.send::<(), R>(path, Method::DELETE, None, options)
483 .await
484 }
485
486 pub async fn delete_with_body<T, R>(
488 &self,
489 path: &str,
490 body: &T,
491 options: RequestOptions,
492 ) -> ApiResult<R>
493 where
494 T: Serialize,
495 R: DeserializeOwned + Default,
496 {
497 self.send(path, Method::DELETE, Some(body), options).await
498 }
499
500 pub async fn patch<T, R>(&self, path: &str, body: &T, options: RequestOptions) -> ApiResult<R>
502 where
503 T: Serialize,
504 R: DeserializeOwned + Default,
505 {
506 self.send(path, Method::PATCH, Some(body), options).await
507 }
508}