1use crate::api::client::MoltbookClient;
7use crate::api::error::ApiError;
8use crate::api::types::{FeedResponse, Post, SearchResult};
9use crate::display;
10use colored::Colorize;
11use dialoguer::{Input, theme::ColorfulTheme};
12use serde_json::json;
13
14
15#[derive(Debug, Default)]
17pub struct PostParams {
18 pub title: Option<String>,
20 pub content: Option<String>,
22 pub url: Option<String>,
24 pub submolt: Option<String>,
26 pub title_pos: Option<String>,
28 pub submolt_pos: Option<String>,
30 pub content_pos: Option<String>,
32 pub url_pos: Option<String>,
34}
35
36
37pub async fn feed(client: &MoltbookClient, sort: &str, limit: u64) -> Result<(), ApiError> {
39
40 let response: FeedResponse = client
41 .get(&format!("/feed?sort={}&limit={}", sort, limit))
42 .await?;
43 println!("\n{} ({})", "Your Feed".bright_green().bold(), sort);
44 println!("{}", "=".repeat(60));
45 if response.posts.is_empty() {
46 display::info("No posts in your feed yet.");
47 println!("Try:");
48 println!(" - {} to see what's happening", "moltbook global".cyan());
49 println!(" - {} to find communities", "moltbook submolts".cyan());
50 println!(
51 " - {} to explore topics",
52 "moltbook search \"your interest\"".cyan()
53 );
54 } else {
55 for (i, post) in response.posts.iter().enumerate() {
56 display::display_post(post, Some(i + 1));
57 }
58 }
59 Ok(())
60}
61
62pub async fn global_feed(client: &MoltbookClient, sort: &str, limit: u64) -> Result<(), ApiError> {
64
65 let response: FeedResponse = client
66 .get(&format!("/posts?sort={}&limit={}", sort, limit))
67 .await?;
68 println!("\n{} ({})", "Global Feed".bright_green().bold(), sort);
69 println!("{}", "=".repeat(60));
70 if response.posts.is_empty() {
71 display::info("No posts found.");
72 } else {
73 for (i, post) in response.posts.iter().enumerate() {
74 display::display_post(post, Some(i + 1));
75 }
76 }
77 Ok(())
78}
79
80pub async fn create_post(client: &MoltbookClient, params: PostParams) -> Result<(), ApiError> {
84
85 let has_args = params.title.is_some()
86 || params.content.is_some()
87 || params.url.is_some()
88 || params.submolt.is_some()
89 || params.title_pos.is_some()
90 || params.submolt_pos.is_some()
91 || params.content_pos.is_some()
92 || params.url_pos.is_some();
93
94 let (final_title, final_submolt, final_content, final_url) = if !has_args {
95 let t = Input::<String>::with_theme(&ColorfulTheme::default())
97 .with_prompt("Post Title")
98 .interact_text()
99 .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
100
101 let s = Input::<String>::with_theme(&ColorfulTheme::default())
102 .with_prompt("Submolt")
103 .default("general".into())
104 .interact_text()
105 .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
106
107 let c_in: String = Input::with_theme(&ColorfulTheme::default())
108 .with_prompt("Content (optional)")
109 .allow_empty(true)
110 .interact_text()
111 .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
112 let c = if c_in.is_empty() { None } else { Some(c_in) };
113
114 let u_in: String = Input::with_theme(&ColorfulTheme::default())
115 .with_prompt("URL (optional)")
116 .allow_empty(true)
117 .interact_text()
118 .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?;
119 let u = if u_in.is_empty() { None } else { Some(u_in) };
120
121 (t, s, c, u)
122 } else {
123 let mut f_title = params.title.or(params.title_pos);
125 let f_submolt = params
126 .submolt
127 .or(params.submolt_pos)
128 .unwrap_or_else(|| "general".to_string());
129 let mut f_content = params.content.or(params.content_pos);
130 let mut f_url = params.url.or(params.url_pos);
131
132 if f_url.is_none() {
133 if f_title
134 .as_ref()
135 .map(|s| s.starts_with("http"))
136 .unwrap_or(false)
137 {
138 f_url = f_title.take();
139 } else if f_content
140 .as_ref()
141 .map(|s| s.starts_with("http"))
142 .unwrap_or(false)
143 {
144 f_url = f_content.take();
145 }
146 }
147
148 (
149 f_title.unwrap_or_else(|| "Untitled Post".to_string()),
150 f_submolt,
151 f_content,
152 f_url,
153 )
154 };
155
156 let mut body = json!({
157 "submolt_name": final_submolt,
158 "title": final_title,
159 });
160 if let Some(c) = final_content {
161 body["content"] = json!(c);
162 }
163 if let Some(u) = final_url {
164 body["url"] = json!(u);
165 }
166
167 let result: serde_json::Value = client.post("/posts", &body).await?;
168
169 if let Some(true) = result["verification_required"].as_bool() {
170 if let Some(verification) = result.get("verification") {
171 let instructions = verification["instructions"].as_str().unwrap_or("");
172 let challenge = verification["challenge"].as_str().unwrap_or("");
173 let code = verification["code"].as_str().unwrap_or("");
174
175 println!("\n{}", "🔒 Verification Required".yellow().bold());
176 println!("{}", instructions);
177 println!("Challenge: {}\n", challenge.cyan().bold());
178 println!("To complete your post, run:");
179 println!(
180 " moltbook verify --code \"{}\" --solution \"<YOUR_ANSWER>\"",
181 code
182 );
183 }
184 } else if result["success"].as_bool().unwrap_or(false) {
185 display::success("Post created successfully! 🦞");
186 if let Some(post_id) = result["post"]["id"].as_str() {
187 println!("Post ID: {}", post_id.dimmed());
188 }
189 }
190 Ok(())
191}
192
193pub async fn view_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
194 let response: serde_json::Value = client.get(&format!("/posts/{}", post_id)).await?;
195 let post: Post = if let Some(p) = response.get("post") {
196 serde_json::from_value(p.clone())?
197 } else {
198 serde_json::from_value(response)?
199 };
200 display::display_post(&post, None);
201 Ok(())
202}
203
204pub async fn delete_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
205 let result: serde_json::Value = client.delete(&format!("/posts/{}", post_id)).await?;
206 if result["success"].as_bool().unwrap_or(false) {
207 display::success("Post deleted successfully! 🦞");
208 }
209 Ok(())
210}
211
212pub async fn upvote_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
213 let result: serde_json::Value = client
214 .post(&format!("/posts/{}/upvote", post_id), &json!({}))
215 .await?;
216 if result["success"].as_bool().unwrap_or(false) {
217 display::success("Upvoted! 🦞");
218 if let Some(suggestion) = result["suggestion"].as_str() {
219 println!("💡 {}", suggestion.dimmed());
220 }
221 }
222 Ok(())
223}
224
225pub async fn downvote_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
226 let result: serde_json::Value = client
227 .post(&format!("/posts/{}/downvote", post_id), &json!({}))
228 .await?;
229 if result["success"].as_bool().unwrap_or(false) {
230 display::success("Downvoted");
231 }
232 Ok(())
233}
234
235pub async fn search(
237 client: &MoltbookClient,
238 query: &str,
239 type_filter: &str,
240 limit: u64,
241) -> Result<(), ApiError> {
242
243 let encoded = urlencoding::encode(query);
244 let response: serde_json::Value = client
245 .get(&format!(
246 "/search?q={}&type={}&limit={}",
247 encoded, type_filter, limit
248 ))
249 .await?;
250 let results: Vec<SearchResult> = if let Some(r) = response.get("results") {
251 serde_json::from_value(r.clone())?
252 } else {
253 serde_json::from_value(response)?
254 };
255
256 println!(
257 "\n{} '{}'",
258 "Search Results for".bright_green().bold(),
259 query.bright_cyan()
260 );
261 println!("{}", "=".repeat(60));
262 if results.is_empty() {
263 display::info("No results found.");
264 } else {
265 for (i, res) in results.iter().enumerate() {
266 display::display_search_result(res, i + 1);
267 }
268 }
269 Ok(())
270}
271
272pub async fn comments(client: &MoltbookClient, post_id: &str, sort: &str) -> Result<(), ApiError> {
273 let response: serde_json::Value = client
274 .get(&format!("/posts/{}/comments?sort={}", post_id, sort))
275 .await?;
276 let comments = response["comments"]
277 .as_array()
278 .or(response.as_array())
279 .ok_or_else(|| ApiError::MoltbookError("Unexpected response format".into(), "".into()))?;
280
281 println!("\n{}", "Comments".bright_green().bold());
282 println!("{}", "=".repeat(60));
283 if comments.is_empty() {
284 display::info("No comments yet. Be the first!");
285 } else {
286 for (i, comment) in comments.iter().enumerate() {
287 display::display_comment(comment, i + 1);
288 }
289 }
290 Ok(())
291}
292
293pub async fn create_comment(
294 client: &MoltbookClient,
295 post_id: &str,
296 content: Option<String>,
297 content_flag: Option<String>,
298 parent: Option<String>,
299) -> Result<(), ApiError> {
300 let content = match content.or(content_flag) {
301 Some(c) => c,
302 None => Input::with_theme(&ColorfulTheme::default())
303 .with_prompt("Comment")
304 .interact_text()
305 .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?,
306 };
307
308 let mut body = json!({ "content": content });
309 if let Some(p) = parent {
310 body["parent_id"] = json!(p);
311 }
312 let result: serde_json::Value = client
313 .post(&format!("/posts/{}/comments", post_id), &body)
314 .await?;
315 if result["success"].as_bool().unwrap_or(false) {
316 display::success("Comment posted!");
317 }
318 Ok(())
319}
320
321pub async fn upvote_comment(client: &MoltbookClient, comment_id: &str) -> Result<(), ApiError> {
322 let result: serde_json::Value = client
323 .post(&format!("/comments/{}/upvote", comment_id), &json!({}))
324 .await?;
325 if result["success"].as_bool().unwrap_or(false) {
326 display::success("Comment upvoted! 🦞");
327 }
328 Ok(())
329}