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 !crate::cli::verification::handle_verification(&result, "post") {
170 if result["success"].as_bool().unwrap_or(false) {
171 display::success("Post created successfully! 🦞");
172 if let Some(post_id) = result["post"]["id"].as_str() {
173 println!("Post ID: {}", post_id.dimmed());
174 }
175 }
176 }
177 Ok(())
178}
179
180pub async fn view_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
181 let response: serde_json::Value = client.get(&format!("/posts/{}", post_id)).await?;
182 let post: Post = if let Some(p) = response.get("post") {
183 serde_json::from_value(p.clone())?
184 } else {
185 serde_json::from_value(response)?
186 };
187 display::display_post(&post, None);
188 Ok(())
189}
190
191pub async fn delete_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
192 let result: serde_json::Value = client.delete(&format!("/posts/{}", post_id)).await?;
193 if !crate::cli::verification::handle_verification(&result, "post deletion") {
194 if result["success"].as_bool().unwrap_or(false) {
195 display::success("Post deleted successfully! 🦞");
196 }
197 }
198 Ok(())
199}
200
201pub async fn upvote_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
202 let result: serde_json::Value = client
203 .post(&format!("/posts/{}/upvote", post_id), &json!({}))
204 .await?;
205 if !crate::cli::verification::handle_verification(&result, "upvote") {
206 if result["success"].as_bool().unwrap_or(false) {
207 display::success("Upvoted! 🦞");
208 if let Some(suggestion) = result["suggestion"].as_str() {
209 println!("💡 {}", suggestion.dimmed());
210 }
211 }
212 }
213 Ok(())
214}
215
216pub async fn downvote_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
217 let result: serde_json::Value = client
218 .post(&format!("/posts/{}/downvote", post_id), &json!({}))
219 .await?;
220 if !crate::cli::verification::handle_verification(&result, "downvote") {
221 if result["success"].as_bool().unwrap_or(false) {
222 display::success("Downvoted");
223 }
224 }
225 Ok(())
226}
227
228pub async fn search(
230 client: &MoltbookClient,
231 query: &str,
232 type_filter: &str,
233 limit: u64,
234) -> Result<(), ApiError> {
235
236 let encoded = urlencoding::encode(query);
237 let response: serde_json::Value = client
238 .get(&format!(
239 "/search?q={}&type={}&limit={}",
240 encoded, type_filter, limit
241 ))
242 .await?;
243 let results: Vec<SearchResult> = if let Some(r) = response.get("results") {
244 serde_json::from_value(r.clone())?
245 } else {
246 serde_json::from_value(response)?
247 };
248
249 println!(
250 "\n{} '{}'",
251 "Search Results for".bright_green().bold(),
252 query.bright_cyan()
253 );
254 println!("{}", "=".repeat(60));
255 if results.is_empty() {
256 display::info("No results found.");
257 } else {
258 for (i, res) in results.iter().enumerate() {
259 display::display_search_result(res, i + 1);
260 }
261 }
262 Ok(())
263}
264
265pub async fn comments(client: &MoltbookClient, post_id: &str, sort: &str) -> Result<(), ApiError> {
266 let response: serde_json::Value = client
267 .get(&format!("/posts/{}/comments?sort={}", post_id, sort))
268 .await?;
269 let comments = response["comments"]
270 .as_array()
271 .or(response.as_array())
272 .ok_or_else(|| ApiError::MoltbookError("Unexpected response format".into(), "".into()))?;
273
274 println!("\n{}", "Comments".bright_green().bold());
275 println!("{}", "=".repeat(60));
276 if comments.is_empty() {
277 display::info("No comments yet. Be the first!");
278 } else {
279 for (i, comment) in comments.iter().enumerate() {
280 display::display_comment(comment, i + 1);
281 }
282 }
283 Ok(())
284}
285
286pub async fn create_comment(
287 client: &MoltbookClient,
288 post_id: &str,
289 content: Option<String>,
290 content_flag: Option<String>,
291 parent: Option<String>,
292) -> Result<(), ApiError> {
293 let content = match content.or(content_flag) {
294 Some(c) => c,
295 None => Input::with_theme(&ColorfulTheme::default())
296 .with_prompt("Comment")
297 .interact_text()
298 .map_err(|e| ApiError::IoError(std::io::Error::other(e)))?,
299 };
300
301 let mut body = json!({ "content": content });
302 if let Some(p) = parent {
303 body["parent_id"] = json!(p);
304 }
305 let result: serde_json::Value = client
306 .post(&format!("/posts/{}/comments", post_id), &body)
307 .await?;
308
309 if !crate::cli::verification::handle_verification(&result, "comment") {
310 if result["success"].as_bool().unwrap_or(false) {
311 display::success("Comment posted!");
312 }
313 }
314 Ok(())
315}
316
317pub async fn upvote_comment(client: &MoltbookClient, comment_id: &str) -> Result<(), ApiError> {
318 let result: serde_json::Value = client
319 .post(&format!("/comments/{}/upvote", comment_id), &json!({}))
320 .await?;
321 if !crate::cli::verification::handle_verification(&result, "comment upvote") {
322 if result["success"].as_bool().unwrap_or(false) {
323 display::success("Comment upvoted! 🦞");
324 }
325 }
326 Ok(())
327}