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 channels = crate::commands::pagination::walk_cursor(
163 cli,
164 &client,
165 &["data", "channels"],
166 |cursor| {
167 let mut qs: Vec<String> = Vec::new();
168 if include_closed {
169 qs.push("include_closed=true".to_string());
170 }
171 if let Some(c) = cursor {
172 qs.push(format!("cursor={}", c));
173 }
174 if qs.is_empty() {
175 format!("{}/channels", base)
176 } else {
177 format!("{}/channels?{}", base, qs.join("&"))
178 }
179 },
180 )
181 .await?;
182 output.print_items(&channels, CHANNEL_FIELDS, "id");
183 Ok(())
184 }
185 ChatCommands::ChannelCreate { name, visibility } => {
186 let mut body = serde_json::json!({ "name": name });
187 if let Some(v) = visibility {
188 body["visibility"] = serde_json::Value::String(v);
189 }
190 let resp = client.post(&format!("{}/channels", base), &body).await?;
191 output.print_single(&resp, CHANNEL_FIELDS, "id");
192 Ok(())
193 }
194 ChatCommands::ChannelGet { id } => {
195 let resp = client.get(&format!("{}/channels/{}", base, id)).await?;
196 output.print_single(&resp, CHANNEL_FIELDS, "id");
197 Ok(())
198 }
199 ChatCommands::ChannelUpdate { id, name, topic } => {
200 let mut body = serde_json::Map::new();
201 if let Some(n) = name {
202 body.insert("name".into(), serde_json::Value::String(n));
203 }
204 if let Some(t) = topic {
205 body.insert("topic".into(), serde_json::Value::String(t));
206 }
207 let resp = client
208 .patch(
209 &format!("{}/channels/{}", base, id),
210 &serde_json::Value::Object(body),
211 )
212 .await?;
213 output.print_single(&resp, CHANNEL_FIELDS, "id");
214 Ok(())
215 }
216 ChatCommands::ChannelDelete { id } => {
217 client.delete(&format!("{}/channels/{}", base, id)).await?;
218 output.print_message(&format!("Channel {} deleted", id));
219 Ok(())
220 }
221 ChatCommands::ChannelFollowers { id } => {
222 let followers =
223 crate::commands::pagination::walk_cursor(cli, &client, &["data"], |cursor| {
224 match cursor {
225 Some(c) => format!("{}/channels/{}/followers?cursor={}", base, id, c),
226 None => format!("{}/channels/{}/followers", base, id),
227 }
228 })
229 .await?;
230 output.print_items(&followers, &["id", "name", "username", "email"], "id");
231 Ok(())
232 }
233 ChatCommands::ChannelMembers { id } => {
234 let members =
235 crate::commands::pagination::walk_cursor(cli, &client, &["data"], |cursor| {
236 match cursor {
237 Some(c) => format!("{}/channels/{}/members?cursor={}", base, id, c),
238 None => format!("{}/channels/{}/members", base, id),
239 }
240 })
241 .await?;
242 output.print_items(&members, &["id", "name", "username", "email"], "id");
243 Ok(())
244 }
245 ChatCommands::Dm { user_ids } => {
246 let body = serde_json::json!({ "user_ids": user_ids });
247 let resp = client
248 .post(&format!("{}/channels/direct_message", base), &body)
249 .await?;
250 output.print_single(&resp, CHANNEL_FIELDS, "id");
251 Ok(())
252 }
253 ChatCommands::MessageList { channel } => {
254 let messages = crate::commands::pagination::walk_cursor(
255 cli,
256 &client,
257 &["data", "messages"],
258 |cursor| match cursor {
259 Some(c) => format!("{}/channels/{}/messages?cursor={}", base, channel, c),
260 None => format!("{}/channels/{}/messages", base, channel),
261 },
262 )
263 .await?;
264 output.print_items(&messages, MESSAGE_FIELDS, "id");
265 Ok(())
266 }
267 ChatCommands::MessageSend {
268 channel,
269 text,
270 r#type,
271 } => {
272 let body = serde_json::json!({ "content": text, "type": r#type });
273 let resp = client
274 .post(&format!("{}/channels/{}/messages", base, channel), &body)
275 .await?;
276 output.print_single(&resp, MESSAGE_FIELDS, "id");
277 Ok(())
278 }
279 ChatCommands::MessageUpdate { id, text } => {
280 let body = serde_json::json!({ "content": text });
281 let resp = client
282 .patch(&format!("{}/messages/{}", base, id), &body)
283 .await?;
284 output.print_single(&resp, MESSAGE_FIELDS, "id");
285 Ok(())
286 }
287 ChatCommands::MessageDelete { id } => {
288 client.delete(&format!("{}/messages/{}", base, id)).await?;
289 output.print_message(&format!("Message {} deleted", id));
290 Ok(())
291 }
292 ChatCommands::ReactionList { msg_id } => {
293 let reactions =
294 crate::commands::pagination::walk_cursor(cli, &client, &["data"], |cursor| {
295 match cursor {
296 Some(c) => format!("{}/messages/{}/reactions?cursor={}", base, msg_id, c),
297 None => format!("{}/messages/{}/reactions", base, msg_id),
298 }
299 })
300 .await?;
301 output.print_items(&reactions, &["reaction", "user", "date"], "reaction");
302 Ok(())
303 }
304 ChatCommands::ReactionAdd { msg_id, emoji } => {
305 let body = serde_json::json!({ "reaction": emoji });
306 let resp = client
307 .post(&format!("{}/messages/{}/reactions", base, msg_id), &body)
308 .await?;
309 println!("{}", serde_json::to_string_pretty(&resp).unwrap());
310 Ok(())
311 }
312 ChatCommands::ReactionRemove { msg_id, emoji } => {
313 let encoded: String = emoji
316 .bytes()
317 .flat_map(|byte| match byte {
318 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
319 vec![byte as char]
320 }
321 _ => format!("%{:02X}", byte).chars().collect(),
322 })
323 .collect();
324 client
325 .delete(&format!(
326 "{}/messages/{}/reactions/{}",
327 base, msg_id, encoded
328 ))
329 .await?;
330 output.print_message(&format!(
331 "Reaction '{}' removed from message {}",
332 emoji, msg_id
333 ));
334 Ok(())
335 }
336 ChatCommands::ReplyList { msg_id } => {
337 let replies = crate::commands::pagination::walk_cursor(
338 cli,
339 &client,
340 &["data", "replies"],
341 |cursor| match cursor {
342 Some(c) => format!("{}/messages/{}/replies?cursor={}", base, msg_id, c),
343 None => format!("{}/messages/{}/replies", base, msg_id),
344 },
345 )
346 .await?;
347 output.print_items(&replies, MESSAGE_FIELDS, "id");
348 Ok(())
349 }
350 ChatCommands::ReplySend { msg_id, text } => {
351 let body = serde_json::json!({ "content": text });
352 let resp = client
353 .post(&format!("{}/messages/{}/replies", base, msg_id), &body)
354 .await?;
355 output.print_single(&resp, MESSAGE_FIELDS, "id");
356 Ok(())
357 }
358 ChatCommands::TaggedUsers { msg_id } => {
359 let users =
360 crate::commands::pagination::walk_cursor(cli, &client, &["data"], |cursor| {
361 match cursor {
362 Some(c) => {
363 format!("{}/messages/{}/tagged_users?cursor={}", base, msg_id, c)
364 }
365 None => format!("{}/messages/{}/tagged_users", base, msg_id),
366 }
367 })
368 .await?;
369 output.print_items(&users, &["id", "name", "username", "email"], "id");
370 Ok(())
371 }
372 }
373}