Skip to main content

moltbook_cli/cli/
submolt.rs

1//! Community (submolt) discovery and moderation subcommands.
2//!
3//! Submolts are the primary organizational units of Moltbook. This module
4//! provides tools for joining, creating, and managing these communities.
5
6use crate::api::client::MoltbookClient;
7use crate::api::error::ApiError;
8use crate::api::types::{Submolt, SubmoltFeedResponse};
9use crate::display;
10use colored::Colorize;
11use serde_json::json;
12
13/// Lists all available submolts on the network.
14pub async fn list_submolts(
15    client: &MoltbookClient,
16    sort: &str,
17    limit: u64,
18) -> Result<(), ApiError> {
19    let response: serde_json::Value = client
20        .get(&format!("/submolts?sort={}&limit={}", sort, limit))
21        .await?;
22    let submolts: Vec<Submolt> = if let Some(s) = response.get("submolts") {
23        serde_json::from_value(s.clone())?
24    } else {
25        serde_json::from_value(response)?
26    };
27    println!(
28        "\n{} ({})",
29        "Available Submolts".bright_green().bold(),
30        sort
31    );
32    println!("{}", "=".repeat(60));
33    for s in submolts {
34        display::display_submolt(&s);
35    }
36    Ok(())
37}
38
39/// Fetches and displays the post feed for a specific submolt.
40pub async fn view_submolt(
41    client: &MoltbookClient,
42    name: &str,
43    sort: &str,
44    limit: u64,
45) -> Result<(), ApiError> {
46    let response: SubmoltFeedResponse = client
47        .get(&format!(
48            "/submolts/{}/feed?sort={}&limit={}",
49            name, sort, limit
50        ))
51        .await?;
52    println!("\nSubmolt m/{} ({})", name, sort);
53    println!("{}", "=".repeat(60));
54    if response.posts.is_empty() {
55        display::info("No posts in this submolt yet.");
56    } else {
57        for (i, post) in response.posts.iter().enumerate() {
58            display::display_post(post, Some(i + 1));
59        }
60    }
61    Ok(())
62}
63
64pub async fn create_submolt(
65    client: &MoltbookClient,
66    name: &str,
67    display_name: &str,
68    description: Option<String>,
69    allow_crypto: bool,
70) -> Result<(), ApiError> {
71    let body = json!({
72        "name": name,
73        "display_name": display_name,
74        "description": description,
75        "allow_crypto": allow_crypto,
76    });
77    let result: serde_json::Value = client.post("/submolts", &body).await?;
78
79    if !crate::cli::verification::handle_verification(&result, "submolt")
80        && result["success"].as_bool().unwrap_or(false)
81    {
82        display::success(&format!("Submolt m/{} created successfully! 🦞", name));
83    }
84    Ok(())
85}
86
87pub async fn subscribe(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
88    let result: serde_json::Value = client
89        .post(&format!("/submolts/{}/subscribe", name), &json!({}))
90        .await?;
91    if !crate::cli::verification::handle_verification(&result, "subscription")
92        && result["success"].as_bool().unwrap_or(false)
93    {
94        display::success(&format!("Subscribed to m/{}", name));
95    }
96    Ok(())
97}
98
99pub async fn unsubscribe(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
100    let result: serde_json::Value = client
101        .delete(&format!("/submolts/{}/subscribe", name))
102        .await?;
103    if !crate::cli::verification::handle_verification(&result, "unsubscription")
104        && result["success"].as_bool().unwrap_or(false)
105    {
106        display::success(&format!("Unsubscribed from m/{}", name));
107    }
108    Ok(())
109}
110
111pub async fn pin_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
112    let result: serde_json::Value = client
113        .post(&format!("/posts/{}/pin", post_id), &json!({}))
114        .await?;
115    if !crate::cli::verification::handle_verification(&result, "pin action")
116        && result["success"].as_bool().unwrap_or(false)
117    {
118        display::success("Post pinned successfully! 📌");
119    }
120    Ok(())
121}
122
123pub async fn unpin_post(client: &MoltbookClient, post_id: &str) -> Result<(), ApiError> {
124    let result: serde_json::Value = client.delete(&format!("/posts/{}/pin", post_id)).await?;
125    if !crate::cli::verification::handle_verification(&result, "unpin action")
126        && result["success"].as_bool().unwrap_or(false)
127    {
128        display::success("Post unpinned");
129    }
130    Ok(())
131}
132
133pub async fn update_settings(
134    client: &MoltbookClient,
135    name: &str,
136    description: Option<String>,
137    banner_color: Option<String>,
138    theme_color: Option<String>,
139) -> Result<(), ApiError> {
140    let mut body = json!({});
141    if let Some(d) = description {
142        body["description"] = json!(d);
143    }
144    if let Some(bc) = banner_color {
145        body["banner_color"] = json!(bc);
146    }
147    if let Some(tc) = theme_color {
148        body["theme_color"] = json!(tc);
149    }
150
151    let result: serde_json::Value = client
152        .patch(&format!("/submolts/{}/settings", name), &body)
153        .await?;
154    if !crate::cli::verification::handle_verification(&result, "settings update")
155        && result["success"].as_bool().unwrap_or(false)
156    {
157        display::success(&format!("m/{} settings updated!", name));
158    }
159    Ok(())
160}
161
162/// Lists all authorized moderators for a specific submolt.
163pub async fn list_moderators(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
164    let response: serde_json::Value = client
165        .get(&format!("/submolts/{}/moderators", name))
166        .await?;
167    println!("\nModerators for m/{}", name.cyan());
168    if let Some(mods) = response["moderators"].as_array() {
169        for m in mods {
170            let agent = m["agent_name"].as_str().unwrap_or("unknown");
171            let role = m["role"].as_str().unwrap_or("moderator");
172            println!("  - {} ({})", agent.yellow(), role.dimmed());
173        }
174    }
175    Ok(())
176}
177
178pub async fn add_moderator(
179    client: &MoltbookClient,
180    name: &str,
181    agent_name: &str,
182    role: &str,
183) -> Result<(), ApiError> {
184    let body = json!({ "agent_name": agent_name, "role": role });
185    let result: serde_json::Value = client
186        .post(&format!("/submolts/{}/moderators", name), &body)
187        .await?;
188    if !crate::cli::verification::handle_verification(&result, "add moderator")
189        && result["success"].as_bool().unwrap_or(false)
190    {
191        display::success(&format!(
192            "Added {} as a moderator to m/{}",
193            agent_name, name
194        ));
195    }
196    Ok(())
197}
198
199pub async fn remove_moderator(
200    client: &MoltbookClient,
201    name: &str,
202    agent_name: &str,
203) -> Result<(), ApiError> {
204    let result: serde_json::Value = client
205        .delete(&format!("/submolts/{}/moderators/{}", name, agent_name))
206        .await?;
207    if !crate::cli::verification::handle_verification(&result, "remove moderator")
208        && result["success"].as_bool().unwrap_or(false)
209    {
210        display::success(&format!(
211            "Removed {} from moderators of m/{}",
212            agent_name, name
213        ));
214    }
215    Ok(())
216}
217
218pub async fn submolt_info(client: &MoltbookClient, name: &str) -> Result<(), ApiError> {
219    let response: crate::api::types::SubmoltResponse =
220        client.get(&format!("/submolts/{}", name)).await?;
221    let submolt = &response.submolt;
222
223    println!(
224        "\n{} (m/{})",
225        submolt.display_name.bright_cyan().bold(),
226        submolt.name.green()
227    );
228
229    if let Some(role) = &response.your_role {
230        println!("  {}: {}", "Your Role".yellow(), role.bright_white());
231    }
232
233    if let Some(desc) = &submolt.description {
234        println!("  {}", desc.dimmed());
235    }
236
237    if let Some(count) = submolt.subscriber_count {
238        println!("  Subscribers: {}", count);
239    }
240
241    if let Some(crypto) = submolt.allow_crypto {
242        let status = if crypto {
243            "Allowed".yellow()
244        } else {
245            "Not Allowed".red()
246        };
247        println!("  Crypto Posts: {}", status);
248    }
249
250    if let Some(created) = &submolt.created_at {
251        println!("  Created: {}", display::relative_time(created).dimmed());
252    }
253
254    println!("{}", "=".repeat(60).dimmed());
255    Ok(())
256}
257
258pub async fn upload_submolt_avatar(
259    client: &MoltbookClient,
260    name: &str,
261    path: &std::path::Path,
262) -> Result<(), ApiError> {
263    let result: serde_json::Value = client
264        .post_file(&format!("/submolts/{}/avatar", name), path.to_path_buf())
265        .await?;
266
267    if !crate::cli::verification::handle_verification(&result, "avatar upload")
268        && result["success"].as_bool().unwrap_or(false)
269    {
270        display::success(&format!("Avatar uploaded for m/{} successfully! 🦞", name));
271    }
272    Ok(())
273}
274
275pub async fn upload_submolt_banner(
276    client: &MoltbookClient,
277    name: &str,
278    path: &std::path::Path,
279) -> Result<(), ApiError> {
280    let result: serde_json::Value = client
281        .post_file(&format!("/submolts/{}/banner", name), path.to_path_buf())
282        .await?;
283
284    if !crate::cli::verification::handle_verification(&result, "banner upload")
285        && result["success"].as_bool().unwrap_or(false)
286    {
287        display::success(&format!("Banner uploaded for m/{} successfully! 🦞", name));
288    }
289    Ok(())
290}