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