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;
13
14const DEFAULT_API_BASE: &str = "https://www.moltbook.com/api/v1";
16
17pub struct MoltbookClient {
22 client: Client,
23 api_key: String,
24 debug: bool,
25 base_url: String,
26}
27
28impl MoltbookClient {
29 pub fn new(api_key: String, debug: bool) -> Self {
36 Self {
37 client: Client::new(),
38 api_key,
39 debug,
40 base_url: DEFAULT_API_BASE.to_string(),
41 }
42 }
43
44 pub fn with_base_url(mut self, base_url: String) -> Self {
46 self.base_url = base_url;
47 self
48 }
49
50 pub async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
56 let url = format!("{}{}", self.base_url, endpoint);
57
58 if self.debug {
59 eprintln!("GET {}", url);
60 }
61
62 let response = self
63 .client
64 .get(&url)
65 .header("Authorization", format!("Bearer {}", self.api_key))
66 .send()
67 .await?;
68
69 self.handle_response(response).await
70 }
71
72 pub async fn post<T: DeserializeOwned>(
78 &self,
79 endpoint: &str,
80 body: &impl Serialize,
81 ) -> Result<T, ApiError> {
82 let url = format!("{}{}", self.base_url, endpoint);
83
84 if self.debug {
85 eprintln!("POST {}", url);
86 eprintln!(
87 "Body: {}",
88 serde_json::to_string_pretty(&body).unwrap_or_default()
89 );
90 }
91
92 let response = self
93 .client
94 .post(&url)
95 .header("Authorization", format!("Bearer {}", self.api_key))
96 .header("Content-Type", "application/json")
97 .json(body)
98 .send()
99 .await?;
100
101 self.handle_response(response).await
102 }
103
104 pub async fn post_file<T: DeserializeOwned>(
112 &self,
113 endpoint: &str,
114 file_path: PathBuf,
115 ) -> Result<T, ApiError> {
116 let url = format!("{}{}", self.base_url, endpoint);
117
118 let file_name = file_path
119 .file_name()
120 .unwrap_or_default()
121 .to_string_lossy()
122 .to_string();
123
124 let file_contents = std::fs::read(&file_path).map_err(ApiError::IoError)?;
125
126 let mime_type = from_path(&file_path).first_or_octet_stream();
127 let part = reqwest::multipart::Part::bytes(file_contents)
128 .file_name(file_name)
129 .mime_str(mime_type.as_ref())?;
130 let form = reqwest::multipart::Form::new().part("file", part);
131
132 if self.debug {
133 eprintln!("POST (File) {}", url);
134 eprintln!("File: {:?}", file_path);
135 }
136
137 let response = self
138 .client
139 .post(&url)
140 .header("Authorization", format!("Bearer {}", self.api_key))
141 .multipart(form)
142 .send()
143 .await?;
144
145 self.handle_response(response).await
146 }
147
148 pub async fn patch<T: DeserializeOwned>(
150 &self,
151 endpoint: &str,
152 body: &impl Serialize,
153 ) -> Result<T, ApiError> {
154 let url = format!("{}{}", self.base_url, endpoint);
155
156 if self.debug {
157 eprintln!("PATCH {}", url);
158 eprintln!(
159 "Body: {}",
160 serde_json::to_string_pretty(&body).unwrap_or_default()
161 );
162 }
163
164 let response = self
165 .client
166 .patch(&url)
167 .header("Authorization", format!("Bearer {}", self.api_key))
168 .header("Content-Type", "application/json")
169 .json(body)
170 .send()
171 .await?;
172
173 self.handle_response(response).await
174 }
175
176 pub async fn delete<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
178 let url = format!("{}{}", self.base_url, endpoint);
179
180 if self.debug {
181 eprintln!("DELETE {}", url);
182 }
183
184 let response = self
185 .client
186 .delete(&url)
187 .header("Authorization", format!("Bearer {}", self.api_key))
188 .send()
189 .await?;
190
191 self.handle_response(response).await
192 }
193
194 async fn handle_response<T: DeserializeOwned>(
202 &self,
203 response: reqwest::Response,
204 ) -> Result<T, ApiError> {
205 let status = response.status();
206 let text = response.text().await?;
207
208 if self.debug {
209 eprintln!("Response Status: {}", status);
210 eprintln!("Response Body: {}", text);
211 }
212
213 if status.as_u16() == 429 {
214 if let Ok(json) = serde_json::from_str::<Value>(&text) {
215 if let Some(retry) = json.get("retry_after_minutes").and_then(|v| v.as_u64()) {
216 return Err(ApiError::RateLimited(format!("{} minutes", retry)));
217 }
218 if let Some(retry) = json.get("retry_after_seconds").and_then(|v| v.as_u64()) {
219 return Err(ApiError::RateLimited(format!("{} seconds", retry)));
220 }
221 }
222 return Err(ApiError::RateLimited("Wait before retrying".to_string()));
223 }
224
225 if !status.is_success() {
226 if let Ok(json) = serde_json::from_str::<Value>(&text) {
227 let error = json
228 .get("error")
229 .and_then(|v| v.as_str())
230 .unwrap_or("Unknown error");
231
232 if error == "captcha_required" {
233 let token = json
234 .get("token")
235 .and_then(|v| v.as_str())
236 .unwrap_or("unknown_token");
237 return Err(ApiError::CaptchaRequired(token.to_string()));
238 }
239
240 let hint = json.get("hint").and_then(|v| v.as_str()).unwrap_or("");
241 return Err(ApiError::MoltbookError(error.to_string(), hint.to_string()));
242 }
243 return Err(ApiError::MoltbookError(format!("HTTP {}", status), text));
244 }
245
246 serde_json::from_str(&text).map_err(ApiError::ParseError)
247 }
248}