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
141 pub async fn patch<T: DeserializeOwned>(
143 &self,
144 endpoint: &str,
145 body: &impl Serialize,
146 ) -> Result<T, ApiError> {
147 let url = format!("{}{}", API_BASE, endpoint);
148
149 if self.debug {
150 eprintln!("PATCH {}", url);
151 eprintln!(
152 "Body: {}",
153 serde_json::to_string_pretty(&body).unwrap_or_default()
154 );
155 }
156
157 let response = self
158 .client
159 .patch(&url)
160 .header("Authorization", format!("Bearer {}", self.api_key))
161 .header("Content-Type", "application/json")
162 .json(body)
163 .send()
164 .await?;
165
166 self.handle_response(response).await
167 }
168
169 pub async fn delete<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
171 let url = format!("{}{}", API_BASE, endpoint);
172
173 if self.debug {
174 eprintln!("DELETE {}", url);
175 }
176
177 let response = self
178 .client
179 .delete(&url)
180 .header("Authorization", format!("Bearer {}", self.api_key))
181 .send()
182 .await?;
183
184 self.handle_response(response).await
185 }
186
187 async fn handle_response<T: DeserializeOwned>(
195 &self,
196 response: reqwest::Response,
197 ) -> Result<T, ApiError> {
198 let status = response.status();
199 let text = response.text().await?;
200
201 if self.debug {
202 eprintln!("Response Status: {}", status);
203 eprintln!("Response Body: {}", text);
204 }
205
206 if status.as_u16() == 429 {
207 if let Ok(json) = serde_json::from_str::<Value>(&text) {
208 if let Some(retry) = json.get("retry_after_minutes").and_then(|v| v.as_u64()) {
209 return Err(ApiError::RateLimited(format!("{} minutes", retry)));
210 }
211 if let Some(retry) = json.get("retry_after_seconds").and_then(|v| v.as_u64()) {
212 return Err(ApiError::RateLimited(format!("{} seconds", retry)));
213 }
214 }
215 return Err(ApiError::RateLimited("Wait before retrying".to_string()));
216 }
217
218 if !status.is_success() {
219 if let Ok(json) = serde_json::from_str::<Value>(&text) {
220 let error = json
221 .get("error")
222 .and_then(|v| v.as_str())
223 .unwrap_or("Unknown error");
224
225 if error == "captcha_required" {
226 let token = json
227 .get("token")
228 .and_then(|v| v.as_str())
229 .unwrap_or("unknown_token");
230 return Err(ApiError::CaptchaRequired(token.to_string()));
231 }
232
233 let hint = json.get("hint").and_then(|v| v.as_str()).unwrap_or("");
234 return Err(ApiError::MoltbookError(error.to_string(), hint.to_string()));
235 }
236 return Err(ApiError::MoltbookError(format!("HTTP {}", status), text));
237 }
238
239 serde_json::from_str(&text).map_err(ApiError::ParseError)
240 }
241}