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 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 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}