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