moltbook_cli/api/
client.rs1use crate::api::error::ApiError;
8use mime_guess::from_path;
9use reqwest::Client;
10use serde::{Serialize, de::DeserializeOwned};
11use serde_json::Value;
12use std::path::PathBuf;
13use std::time::Duration;
14
15const DEFAULT_API_BASE: &str = "https://www.moltbook.com/api/v1";
17
18pub struct MoltbookClient {
23 client: Client,
24 api_key: String,
25 pub agent_name: String,
26 debug: bool,
27 base_url: String,
28}
29
30impl MoltbookClient {
31 pub fn new(api_key: String, agent_name: String, debug: bool) -> Self {
38 Self {
39 client: Client::builder()
40 .timeout(Duration::from_secs(30))
41 .connect_timeout(Duration::from_secs(10))
42 .build()
43 .expect("Failed to build HTTP client"),
44 api_key,
45 agent_name,
46 debug,
47 base_url: DEFAULT_API_BASE.to_string(),
48 }
49 }
50
51 pub fn with_base_url(mut self, base_url: String) -> Self {
53 self.base_url = base_url;
54 self
55 }
56
57 pub async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
63 let url = format!("{}{}", self.base_url, endpoint);
64
65 if self.debug {
66 eprintln!("GET {}", url);
67 }
68
69 let response = self
70 .client
71 .get(&url)
72 .header("Authorization", format!("Bearer {}", self.api_key))
73 .send()
74 .await?;
75
76 self.handle_response(response).await
77 }
78
79 pub async fn post<T: DeserializeOwned>(
85 &self,
86 endpoint: &str,
87 body: &impl Serialize,
88 ) -> Result<T, ApiError> {
89 let url = format!("{}{}", self.base_url, endpoint);
90
91 if self.debug {
92 eprintln!("POST {}", url);
93 eprintln!(
94 "Body: {}",
95 serde_json::to_string_pretty(&body).unwrap_or_default()
96 );
97 }
98
99 let response = self
100 .client
101 .post(&url)
102 .header("Authorization", format!("Bearer {}", self.api_key))
103 .header("Content-Type", "application/json")
104 .json(body)
105 .send()
106 .await?;
107
108 self.handle_response(response).await
109 }
110
111 pub async fn post_unauth<T: DeserializeOwned>(
113 &self,
114 endpoint: &str,
115 body: &impl Serialize,
116 ) -> Result<T, ApiError> {
117 let url = format!("{}{}", self.base_url, endpoint);
118
119 if self.debug {
120 eprintln!("POST (unauth) {}", url);
121 eprintln!(
122 "Body: {}",
123 serde_json::to_string_pretty(&body).unwrap_or_default()
124 );
125 }
126
127 let response = self
128 .client
129 .post(&url)
130 .header("Content-Type", "application/json")
131 .json(body)
132 .send()
133 .await?;
134
135 self.handle_response(response).await
136 }
137
138 pub async fn post_file<T: DeserializeOwned>(
146 &self,
147 endpoint: &str,
148 file_path: PathBuf,
149 ) -> Result<T, ApiError> {
150 let url = format!("{}{}", self.base_url, endpoint);
151
152 let file_name = file_path
153 .file_name()
154 .unwrap_or_default()
155 .to_string_lossy()
156 .to_string();
157
158 let file_contents = std::fs::read(&file_path).map_err(ApiError::IoError)?;
159
160 let mime_type = from_path(&file_path).first_or_octet_stream();
161 let part = reqwest::multipart::Part::bytes(file_contents)
162 .file_name(file_name)
163 .mime_str(mime_type.as_ref())?;
164 let form = reqwest::multipart::Form::new().part("file", part);
165
166 if self.debug {
167 eprintln!("POST (File) {}", url);
168 eprintln!("File: {:?}", file_path);
169 }
170
171 let response = self
172 .client
173 .post(&url)
174 .header("Authorization", format!("Bearer {}", self.api_key))
175 .multipart(form)
176 .send()
177 .await?;
178
179 self.handle_response(response).await
180 }
181
182 pub async fn patch<T: DeserializeOwned>(
184 &self,
185 endpoint: &str,
186 body: &impl Serialize,
187 ) -> Result<T, ApiError> {
188 let url = format!("{}{}", self.base_url, endpoint);
189
190 if self.debug {
191 eprintln!("PATCH {}", url);
192 eprintln!(
193 "Body: {}",
194 serde_json::to_string_pretty(&body).unwrap_or_default()
195 );
196 }
197
198 let response = self
199 .client
200 .patch(&url)
201 .header("Authorization", format!("Bearer {}", self.api_key))
202 .header("Content-Type", "application/json")
203 .json(body)
204 .send()
205 .await?;
206
207 self.handle_response(response).await
208 }
209
210 pub async fn delete<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
212 let url = format!("{}{}", self.base_url, endpoint);
213
214 if self.debug {
215 eprintln!("DELETE {}", url);
216 }
217
218 let response = self
219 .client
220 .delete(&url)
221 .header("Authorization", format!("Bearer {}", self.api_key))
222 .send()
223 .await?;
224
225 self.handle_response(response).await
226 }
227
228 async fn handle_response<T: DeserializeOwned>(
236 &self,
237 response: reqwest::Response,
238 ) -> Result<T, ApiError> {
239 let status = response.status();
240 let text = response.text().await?;
241
242 if self.debug {
243 eprintln!("Response Status: {}", status);
244 eprintln!("Response Body: {}", text);
245 }
246
247 if status.as_u16() == 429 {
248 if let Ok(json) = serde_json::from_str::<Value>(&text) {
249 if let Some(retry) = json.get("retry_after_minutes").and_then(|v| v.as_u64()) {
250 return Err(ApiError::RateLimited(format!("{} minutes", retry)));
251 }
252 if let Some(retry) = json.get("retry_after_seconds").and_then(|v| v.as_u64()) {
253 return Err(ApiError::RateLimited(format!("{} seconds", retry)));
254 }
255 }
256 return Err(ApiError::RateLimited("Wait before retrying".to_string()));
257 }
258
259 if !status.is_success() {
260 if let Ok(json) = serde_json::from_str::<Value>(&text) {
261 let error = json
262 .get("error")
263 .and_then(|v| v.as_str())
264 .unwrap_or("Unknown error");
265
266 if error == "captcha_required" {
267 let token = json
268 .get("token")
269 .and_then(|v| v.as_str())
270 .unwrap_or("unknown_token");
271 return Err(ApiError::CaptchaRequired(token.to_string()));
272 }
273
274 let hint = json.get("hint").and_then(|v| v.as_str()).unwrap_or("");
275 return Err(ApiError::MoltbookError(error.to_string(), hint.to_string()));
276 }
277 return Err(ApiError::MoltbookError(format!("HTTP {}", status), text));
278 }
279
280 serde_json::from_str(&text).map_err(ApiError::ParseError)
281 }
282}