moltbook_cli/api/
client.rs1use crate::api::error::ApiError;
2use mime_guess::from_path;
3use reqwest::Client;
4use serde::{Serialize, de::DeserializeOwned};
5use serde_json::Value;
6use std::path::PathBuf;
7
8const API_BASE: &str = "https://www.moltbook.com/api/v1";
9
10pub struct MoltbookClient {
11 client: Client,
12 api_key: String,
13 debug: bool,
14}
15
16impl MoltbookClient {
17 pub fn new(api_key: String, debug: bool) -> Self {
18 Self {
19 client: Client::new(),
20 api_key,
21 debug,
22 }
23 }
24
25 pub async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
26 let url = format!("{}{}", API_BASE, endpoint);
27
28 if self.debug {
29 eprintln!("GET {}", url);
30 }
31
32 let response = self
33 .client
34 .get(&url)
35 .header("Authorization", format!("Bearer {}", self.api_key))
36 .send()
37 .await?;
38
39 self.handle_response(response).await
40 }
41
42 pub async fn post<T: DeserializeOwned>(
43 &self,
44 endpoint: &str,
45 body: &impl Serialize,
46 ) -> Result<T, ApiError> {
47 let url = format!("{}{}", API_BASE, endpoint);
48
49 if self.debug {
50 eprintln!("POST {}", url);
51 eprintln!(
52 "Body: {}",
53 serde_json::to_string_pretty(&body).unwrap_or_default()
54 );
55 }
56
57 let response = self
58 .client
59 .post(&url)
60 .header("Authorization", format!("Bearer {}", self.api_key))
61 .header("Content-Type", "application/json")
62 .json(body)
63 .send()
64 .await?;
65
66 self.handle_response(response).await
67 }
68
69 pub async fn post_file<T: DeserializeOwned>(
70 &self,
71 endpoint: &str,
72 file_path: PathBuf,
73 ) -> Result<T, ApiError> {
74 let url = format!("{}{}", API_BASE, endpoint);
75
76 let file_name = file_path
77 .file_name()
78 .unwrap_or_default()
79 .to_string_lossy()
80 .to_string();
81
82 let file_contents = std::fs::read(&file_path).map_err(|e| ApiError::IoError(e))?;
83
84 let mime_type = from_path(&file_path).first_or_octet_stream();
85 let part = reqwest::multipart::Part::bytes(file_contents)
86 .file_name(file_name)
87 .mime_str(mime_type.as_ref())?;
88 let form = reqwest::multipart::Form::new().part("file", part);
89
90 if self.debug {
91 eprintln!("POST (File) {}", url);
92 eprintln!("File: {:?}", file_path);
93 }
94
95 let response = self
96 .client
97 .post(&url)
98 .header("Authorization", format!("Bearer {}", self.api_key))
99 .multipart(form)
100 .send()
101 .await?;
102
103 self.handle_response(response).await
104 }
105
106 pub async fn patch<T: DeserializeOwned>(
107 &self,
108 endpoint: &str,
109 body: &impl Serialize,
110 ) -> Result<T, ApiError> {
111 let url = format!("{}{}", API_BASE, endpoint);
112
113 if self.debug {
114 eprintln!("PATCH {}", url);
115 eprintln!(
116 "Body: {}",
117 serde_json::to_string_pretty(&body).unwrap_or_default()
118 );
119 }
120
121 let response = self
122 .client
123 .patch(&url)
124 .header("Authorization", format!("Bearer {}", self.api_key))
125 .header("Content-Type", "application/json")
126 .json(body)
127 .send()
128 .await?;
129
130 self.handle_response(response).await
131 }
132
133 pub async fn delete<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
134 let url = format!("{}{}", API_BASE, endpoint);
135
136 if self.debug {
137 eprintln!("DELETE {}", url);
138 }
139
140 let response = self
141 .client
142 .delete(&url)
143 .header("Authorization", format!("Bearer {}", self.api_key))
144 .send()
145 .await?;
146
147 self.handle_response(response).await
148 }
149
150 async fn handle_response<T: DeserializeOwned>(
151 &self,
152 response: reqwest::Response,
153 ) -> Result<T, ApiError> {
154 let status = response.status();
155 let text = response.text().await?;
156
157 if self.debug {
158 eprintln!("Response Status: {}", status);
159 eprintln!("Response Body: {}", text);
160 }
161
162 if status.as_u16() == 429 {
163 if let Ok(json) = serde_json::from_str::<Value>(&text) {
165 if let Some(retry) = json.get("retry_after_minutes").and_then(|v| v.as_u64()) {
166 return Err(ApiError::RateLimited(format!("{} minutes", retry)));
167 }
168 if let Some(retry) = json.get("retry_after_seconds").and_then(|v| v.as_u64()) {
169 return Err(ApiError::RateLimited(format!("{} seconds", retry)));
170 }
171 }
172 return Err(ApiError::RateLimited("Wait before retrying".to_string()));
173 }
174
175 if !status.is_success() {
177 if let Ok(json) = serde_json::from_str::<Value>(&text) {
178 let error = json
179 .get("error")
180 .and_then(|v| v.as_str())
181 .unwrap_or("Unknown error");
182
183 if error == "captcha_required" {
185 let token = json
186 .get("token")
187 .and_then(|v| v.as_str())
188 .unwrap_or("unknown_token");
189 return Err(ApiError::CaptchaRequired(token.to_string()));
190 }
191
192 let hint = json.get("hint").and_then(|v| v.as_str()).unwrap_or("");
193 return Err(ApiError::MoltbookError(error.to_string(), hint.to_string()));
194 }
195 return Err(ApiError::MoltbookError(format!("HTTP {}", status), text));
196 }
197
198 serde_json::from_str(&text).map_err(ApiError::ParseError)
199 }
200}