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