Skip to main content

moltbook_cli/api/
client.rs

1use 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            // Try to parse rate limit info
164            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        // Handle generic errors
176        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                // Handle Captcha
184                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}