1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::commands::workspace::resolve_workspace;
4use crate::error::CliError;
5use crate::output::OutputConfig;
6use crate::Cli;
7use clap::Subcommand;
8
9#[derive(Subcommand)]
10pub enum ChatCommands {
11 #[command(name = "channel-list")]
13 ChannelList {
14 #[arg(long)]
16 include_closed: bool,
17 },
18 #[command(name = "channel-create")]
20 ChannelCreate {
21 #[arg(long)]
23 name: String,
24 #[arg(long)]
26 visibility: Option<String>,
27 },
28 #[command(name = "channel-get")]
30 ChannelGet {
31 id: String,
33 },
34 #[command(name = "channel-update")]
36 ChannelUpdate {
37 id: String,
39 #[arg(long)]
41 name: Option<String>,
42 #[arg(long)]
44 topic: Option<String>,
45 },
46 #[command(name = "channel-delete")]
48 ChannelDelete {
49 id: String,
51 },
52 #[command(name = "channel-followers")]
54 ChannelFollowers {
55 id: String,
57 },
58 #[command(name = "channel-members")]
60 ChannelMembers {
61 id: String,
63 },
64 Dm {
66 user_ids: Vec<String>,
68 },
69 #[command(name = "message-list")]
71 MessageList {
72 #[arg(long)]
74 channel: String,
75 },
76 #[command(name = "message-send")]
78 MessageSend {
79 #[arg(long)]
81 channel: String,
82 #[arg(long)]
84 text: String,
85 #[arg(long, default_value = "message")]
87 r#type: String,
88 },
89 #[command(name = "message-update")]
91 MessageUpdate {
92 id: String,
94 #[arg(long)]
96 text: String,
97 },
98 #[command(name = "message-delete")]
100 MessageDelete {
101 id: String,
103 },
104 #[command(name = "reaction-list")]
106 ReactionList {
107 msg_id: String,
109 },
110 #[command(name = "reaction-add")]
112 ReactionAdd {
113 msg_id: String,
115 #[arg(long)]
117 emoji: String,
118 },
119 #[command(name = "reaction-remove")]
121 ReactionRemove {
122 msg_id: String,
124 emoji: String,
126 },
127 #[command(name = "reply-list")]
129 ReplyList {
130 msg_id: String,
132 },
133 #[command(name = "reply-send")]
135 ReplySend {
136 msg_id: String,
138 #[arg(long)]
140 text: String,
141 },
142 #[command(name = "tagged-users")]
144 TaggedUsers {
145 msg_id: String,
147 },
148}
149
150const CHANNEL_FIELDS: &[&str] = &["id", "name", "visibility", "type"];
151const MESSAGE_FIELDS: &[&str] = &["id", "content", "type", "date"];
152
153pub async fn execute(command: ChatCommands, cli: &Cli) -> Result<(), CliError> {
154 let token = resolve_token(cli)?;
155 let client = ClickUpClient::new(&token, cli.timeout)?;
156 let ws_id = resolve_workspace(cli)?;
157 let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
158 let base = format!("/v3/workspaces/{}/chat", ws_id);
159
160 match command {
161 ChatCommands::ChannelList { include_closed } => {
162 let query = if include_closed {
163 "?include_closed=true"
164 } else {
165 ""
166 };
167 let resp = client.get(&format!("{}/channels{}", base, query)).await?;
168 let mut channels = resp
170 .get("data")
171 .or_else(|| resp.get("channels"))
172 .and_then(|v| v.as_array())
173 .cloned()
174 .unwrap_or_default();
175 if let Some(limit) = cli.limit {
176 channels.truncate(limit);
177 }
178 output.print_items(&channels, CHANNEL_FIELDS, "id");
179 Ok(())
180 }
181 ChatCommands::ChannelCreate { name, visibility } => {
182 let mut body = serde_json::json!({ "name": name });
183 if let Some(v) = visibility {
184 body["visibility"] = serde_json::Value::String(v);
185 }
186 let resp = client.post(&format!("{}/channels", base), &body).await?;
187 output.print_single(&resp, CHANNEL_FIELDS, "id");
188 Ok(())
189 }
190 ChatCommands::ChannelGet { id } => {
191 let resp = client.get(&format!("{}/channels/{}", base, id)).await?;
192 output.print_single(&resp, CHANNEL_FIELDS, "id");
193 Ok(())
194 }
195 ChatCommands::ChannelUpdate { id, name, topic } => {
196 let mut body = serde_json::Map::new();
197 if let Some(n) = name {
198 body.insert("name".into(), serde_json::Value::String(n));
199 }
200 if let Some(t) = topic {
201 body.insert("topic".into(), serde_json::Value::String(t));
202 }
203 let resp = client
204 .patch(
205 &format!("{}/channels/{}", base, id),
206 &serde_json::Value::Object(body),
207 )
208 .await?;
209 output.print_single(&resp, CHANNEL_FIELDS, "id");
210 Ok(())
211 }
212 ChatCommands::ChannelDelete { id } => {
213 client.delete(&format!("{}/channels/{}", base, id)).await?;
214 output.print_message(&format!("Channel {} deleted", id));
215 Ok(())
216 }
217 ChatCommands::ChannelFollowers { id } => {
218 let resp = client
219 .get(&format!("{}/channels/{}/followers", base, id))
220 .await?;
221 println!("{}", serde_json::to_string_pretty(&resp).unwrap());
222 Ok(())
223 }
224 ChatCommands::ChannelMembers { id } => {
225 let resp = client
226 .get(&format!("{}/channels/{}/members", base, id))
227 .await?;
228 println!("{}", serde_json::to_string_pretty(&resp).unwrap());
229 Ok(())
230 }
231 ChatCommands::Dm { user_ids } => {
232 let body = serde_json::json!({ "user_ids": user_ids });
233 let resp = client
234 .post(&format!("{}/channels/direct_message", base), &body)
235 .await?;
236 output.print_single(&resp, CHANNEL_FIELDS, "id");
237 Ok(())
238 }
239 ChatCommands::MessageList { channel } => {
240 let resp = client
241 .get(&format!("{}/channels/{}/messages", base, channel))
242 .await?;
243 let mut messages = resp
246 .get("data")
247 .or_else(|| resp.get("messages"))
248 .and_then(|v| v.as_array())
249 .cloned()
250 .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
251 if let Some(limit) = cli.limit {
252 messages.truncate(limit);
253 }
254 output.print_items(&messages, MESSAGE_FIELDS, "id");
255 Ok(())
256 }
257 ChatCommands::MessageSend {
258 channel,
259 text,
260 r#type,
261 } => {
262 let body = serde_json::json!({ "content": text, "type": r#type });
263 let resp = client
264 .post(&format!("{}/channels/{}/messages", base, channel), &body)
265 .await?;
266 output.print_single(&resp, MESSAGE_FIELDS, "id");
267 Ok(())
268 }
269 ChatCommands::MessageUpdate { id, text } => {
270 let body = serde_json::json!({ "content": text });
271 let resp = client
272 .patch(&format!("{}/messages/{}", base, id), &body)
273 .await?;
274 output.print_single(&resp, MESSAGE_FIELDS, "id");
275 Ok(())
276 }
277 ChatCommands::MessageDelete { id } => {
278 client.delete(&format!("{}/messages/{}", base, id)).await?;
279 output.print_message(&format!("Message {} deleted", id));
280 Ok(())
281 }
282 ChatCommands::ReactionList { msg_id } => {
283 let resp = client
284 .get(&format!("{}/messages/{}/reactions", base, msg_id))
285 .await?;
286 println!("{}", serde_json::to_string_pretty(&resp).unwrap());
287 Ok(())
288 }
289 ChatCommands::ReactionAdd { msg_id, emoji } => {
290 let body = serde_json::json!({ "reaction": emoji });
291 let resp = client
292 .post(&format!("{}/messages/{}/reactions", base, msg_id), &body)
293 .await?;
294 println!("{}", serde_json::to_string_pretty(&resp).unwrap());
295 Ok(())
296 }
297 ChatCommands::ReactionRemove { msg_id, emoji } => {
298 let encoded: String = emoji
301 .bytes()
302 .flat_map(|byte| match byte {
303 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
304 vec![byte as char]
305 }
306 _ => format!("%{:02X}", byte).chars().collect(),
307 })
308 .collect();
309 client
310 .delete(&format!(
311 "{}/messages/{}/reactions/{}",
312 base, msg_id, encoded
313 ))
314 .await?;
315 output.print_message(&format!(
316 "Reaction '{}' removed from message {}",
317 emoji, msg_id
318 ));
319 Ok(())
320 }
321 ChatCommands::ReplyList { msg_id } => {
322 let resp = client
323 .get(&format!("{}/messages/{}/replies", base, msg_id))
324 .await?;
325 let mut replies = resp
328 .get("data")
329 .or_else(|| resp.get("replies"))
330 .and_then(|v| v.as_array())
331 .cloned()
332 .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
333 if let Some(limit) = cli.limit {
334 replies.truncate(limit);
335 }
336 output.print_items(&replies, MESSAGE_FIELDS, "id");
337 Ok(())
338 }
339 ChatCommands::ReplySend { msg_id, text } => {
340 let body = serde_json::json!({ "content": text });
341 let resp = client
342 .post(&format!("{}/messages/{}/replies", base, msg_id), &body)
343 .await?;
344 output.print_single(&resp, MESSAGE_FIELDS, "id");
345 Ok(())
346 }
347 ChatCommands::TaggedUsers { msg_id } => {
348 let resp = client
349 .get(&format!("{}/messages/{}/tagged_users", base, msg_id))
350 .await?;
351 println!("{}", serde_json::to_string_pretty(&resp).unwrap());
352 Ok(())
353 }
354 }
355}