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