Skip to main content

moltbook_cli/cli/
post.rs

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