1use crate::error::CliError;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4use tokio::time::{sleep, Duration};
5
6pub struct ClickUpClient {
7 http: reqwest::Client,
8 base_url: String,
9 token: String,
10 rate_limit_remaining: Arc<AtomicU64>,
11 rate_limit_reset: Arc<AtomicU64>,
12}
13
14impl ClickUpClient {
15 pub fn new(token: &str, timeout_secs: u64) -> Result<Self, CliError> {
16 let http = reqwest::Client::builder()
17 .timeout(std::time::Duration::from_secs(timeout_secs))
18 .build()
19 .map_err(|e| CliError::ClientError {
20 message: format!("Failed to create HTTP client: {}", e),
21 status: 0,
22 })?;
23 let base_url = std::env::var("CLICKUP_API_URL")
24 .unwrap_or_else(|_| "https://api.clickup.com/api".to_string());
25 Ok(Self {
26 http,
27 base_url,
28 token: token.to_string(),
29 rate_limit_remaining: Arc::new(AtomicU64::new(100)),
30 rate_limit_reset: Arc::new(AtomicU64::new(0)),
31 })
32 }
33
34 fn update_rate_limits(&self, headers: &reqwest::header::HeaderMap) {
35 if let Some(remaining) = headers.get("X-RateLimit-Remaining") {
36 if let Ok(val) = remaining.to_str().unwrap_or("0").parse::<u64>() {
37 self.rate_limit_remaining.store(val, Ordering::Relaxed);
38 }
39 }
40 if let Some(reset) = headers.get("X-RateLimit-Reset") {
41 if let Ok(val) = reset.to_str().unwrap_or("0").parse::<u64>() {
42 self.rate_limit_reset.store(val, Ordering::Relaxed);
43 }
44 }
45 }
46
47 async fn request(
48 &self,
49 method: reqwest::Method,
50 path: &str,
51 body: Option<&serde_json::Value>,
52 ) -> Result<serde_json::Value, CliError> {
53 let url = format!("{}{}", self.base_url, path);
54 let max_retries = 3;
55
56 for attempt in 0..=max_retries {
57 let mut req = self
58 .http
59 .request(method.clone(), &url)
60 .header("Authorization", &self.token)
61 .header("Content-Type", "application/json");
62
63 if let Some(b) = body {
64 req = req.json(b);
65 }
66
67 let resp = req.send().await.map_err(|e| CliError::ClientError {
68 message: format!("Request failed: {}", e),
69 status: 0,
70 })?;
71
72 let status = resp.status().as_u16();
73 self.update_rate_limits(resp.headers());
74
75 if (200..300).contains(&status) {
76 if status == 204 {
77 return Ok(serde_json::json!({}));
78 }
79 let json: serde_json::Value =
80 resp.json().await.map_err(|e| CliError::ClientError {
81 message: format!("Failed to parse response: {}", e),
82 status,
83 })?;
84 return Ok(json);
85 }
86
87 if status == 429 && attempt == 0 {
89 let reset = self.rate_limit_reset.load(Ordering::Relaxed);
90 let now = std::time::SystemTime::now()
91 .duration_since(std::time::UNIX_EPOCH)
92 .unwrap()
93 .as_secs();
94 let wait = if reset > now { reset - now } else { 1 };
95 eprintln!("Rate limited. Waiting {} seconds...", wait);
96 sleep(Duration::from_secs(wait)).await;
97 continue;
98 }
99
100 if (500..=599).contains(&status) && attempt < max_retries {
102 let wait = 1u64 << attempt; eprintln!("Server error ({}). Retrying in {}s...", status, wait);
104 sleep(Duration::from_secs(wait)).await;
105 continue;
106 }
107
108 let body_text = resp.text().await.unwrap_or_default();
110 let message = serde_json::from_str::<serde_json::Value>(&body_text)
111 .ok()
112 .and_then(|v| v.get("err").and_then(|e| e.as_str()).map(String::from))
113 .unwrap_or_else(|| format!("HTTP {}", status));
114
115 return match status {
116 401 => Err(CliError::AuthError { message }),
117 403 => Err(CliError::Forbidden { message }),
118 404 => Err(CliError::NotFound {
119 message,
120 resource_id: String::new(),
121 }),
122 429 => Err(CliError::RateLimited {
123 message,
124 retry_after: None,
125 }),
126 500..=599 => Err(CliError::ServerError { message }),
127 _ => Err(CliError::ClientError { message, status }),
128 };
129 }
130
131 Err(CliError::ServerError {
132 message: "Max retries exceeded".into(),
133 })
134 }
135
136 pub async fn get(&self, path: &str) -> Result<serde_json::Value, CliError> {
137 self.request(reqwest::Method::GET, path, None).await
138 }
139
140 pub async fn post(
141 &self,
142 path: &str,
143 body: &serde_json::Value,
144 ) -> Result<serde_json::Value, CliError> {
145 self.request(reqwest::Method::POST, path, Some(body)).await
146 }
147
148 pub async fn put(
149 &self,
150 path: &str,
151 body: &serde_json::Value,
152 ) -> Result<serde_json::Value, CliError> {
153 self.request(reqwest::Method::PUT, path, Some(body)).await
154 }
155
156 pub async fn delete(&self, path: &str) -> Result<serde_json::Value, CliError> {
157 self.request(reqwest::Method::DELETE, path, None).await
158 }
159
160 pub async fn patch(
161 &self,
162 path: &str,
163 body: &serde_json::Value,
164 ) -> Result<serde_json::Value, CliError> {
165 self.request(reqwest::Method::PATCH, path, Some(body)).await
166 }
167
168 pub async fn delete_with_body(
169 &self,
170 path: &str,
171 body: &serde_json::Value,
172 ) -> Result<serde_json::Value, CliError> {
173 self.request(reqwest::Method::DELETE, path, Some(body))
174 .await
175 }
176
177 pub async fn upload_file(
178 &self,
179 path: &str,
180 file_path: &std::path::Path,
181 ) -> Result<serde_json::Value, CliError> {
182 let url = format!("{}{}", self.base_url, path);
183 let file_name = file_path
184 .file_name()
185 .and_then(|n| n.to_str())
186 .unwrap_or("file")
187 .to_string();
188 let file_bytes = tokio::fs::read(file_path)
189 .await
190 .map_err(CliError::IoError)?;
191 let part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
192 let form = reqwest::multipart::Form::new().part("attachment", part);
193
194 let resp = self
195 .http
196 .post(&url)
197 .header("Authorization", &self.token)
198 .multipart(form)
199 .send()
200 .await
201 .map_err(|e| CliError::ClientError {
202 message: format!("Upload failed: {}", e),
203 status: 0,
204 })?;
205
206 let status = resp.status().as_u16();
207 self.update_rate_limits(resp.headers());
208
209 if (200..300).contains(&status) {
210 if status == 204 {
211 return Ok(serde_json::json!({}));
212 }
213 let json: serde_json::Value = resp.json().await.map_err(|e| CliError::ClientError {
214 message: format!("Failed to parse response: {}", e),
215 status,
216 })?;
217 return Ok(json);
218 }
219
220 let body_text = resp.text().await.unwrap_or_default();
221 let message = serde_json::from_str::<serde_json::Value>(&body_text)
222 .ok()
223 .and_then(|v| v.get("err").and_then(|e| e.as_str()).map(String::from))
224 .unwrap_or_else(|| format!("HTTP {}", status));
225
226 Err(match status {
227 401 => CliError::AuthError { message },
228 404 => CliError::NotFound {
229 message,
230 resource_id: String::new(),
231 },
232 429 => CliError::RateLimited {
233 message,
234 retry_after: None,
235 },
236 500..=599 => CliError::ServerError { message },
237 _ => CliError::ClientError { message, status },
238 })
239 }
240
241 pub fn with_base_url(mut self, base_url: &str) -> Self {
243 self.base_url = base_url.to_string();
244 self
245 }
246}