Skip to main content

slack_rs/cli/
mod.rs

1//! CLI command routing and handlers
2
3mod context;
4mod handlers;
5
6pub use context::CliContext;
7pub use handlers::{handle_export_command, handle_import_command, run_api_call, run_auth_login};
8
9use crate::api::{ApiClient, CommandResponse};
10use crate::commands;
11use crate::commands::ConversationSelector;
12use crate::profile::{
13    create_token_store, default_config_path, load_config, make_token_key, resolve_profile_full,
14    TokenType,
15};
16use serde_json::Value;
17
18/// Get API client for a profile with optional token type selection
19///
20/// # Arguments
21/// * `profile_name` - Optional profile name (defaults to "default")
22/// * `token_type` - Optional token type (bot/user). If None, uses profile default or bot fallback
23///
24/// # Token Resolution Priority
25/// 1. CLI flag token_type parameter (if provided)
26/// 2. Profile's default_token_type (if set)
27/// 3. Try user token first, fall back to bot token
28pub async fn get_api_client_with_token_type(
29    profile_name: Option<String>,
30    token_type: Option<TokenType>,
31) -> Result<ApiClient, String> {
32    let profile_name = profile_name.unwrap_or_else(|| "default".to_string());
33    let config_path = default_config_path().map_err(|e| e.to_string())?;
34    let config = load_config(&config_path).map_err(|e| e.to_string())?;
35
36    let profile = config
37        .get(&profile_name)
38        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
39
40    let token_store = create_token_store().map_err(|e| e.to_string())?;
41
42    // Resolve token type: CLI flag > profile default > try user first with bot fallback
43    let resolved_token_type = token_type.or(profile.default_token_type);
44
45    let bot_token_key = make_token_key(&profile.team_id, &profile.user_id);
46    let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
47
48    let token = match resolved_token_type {
49        Some(TokenType::Bot) => {
50            // Explicitly requested bot token
51            token_store
52                .get(&bot_token_key)
53                .map_err(|e| format!("Failed to get bot token: {}", e))?
54        }
55        Some(TokenType::User) => {
56            // Explicitly requested user token
57            token_store
58                .get(&user_token_key)
59                .map_err(|e| format!("Failed to get user token: {}", e))?
60        }
61        None => {
62            // No explicit preference, try user token first (for APIs that require user scope)
63            match token_store.get(&user_token_key) {
64                Ok(user_token) => user_token,
65                Err(_) => {
66                    // Fall back to bot token
67                    token_store
68                        .get(&bot_token_key)
69                        .map_err(|e| format!("Failed to get token: {}", e))?
70                }
71            }
72        }
73    };
74
75    Ok(ApiClient::with_token(token))
76}
77
78/// Get API client for a profile (legacy function, maintains backward compatibility)
79#[allow(dead_code)]
80pub async fn get_api_client(profile_name: Option<String>) -> Result<ApiClient, String> {
81    get_api_client_with_token_type(profile_name, None).await
82}
83
84/// Check if a flag exists in args
85pub fn has_flag(args: &[String], flag: &str) -> bool {
86    args.iter().any(|arg| arg == flag)
87}
88
89/// Check if error message indicates non-interactive mode failure
90pub fn is_non_interactive_error(error_msg: &str) -> bool {
91    error_msg.contains("Non-interactive mode error")
92        || error_msg.contains("Use --yes flag to confirm in non-interactive mode")
93}
94
95/// Wrap response with unified envelope including metadata
96pub async fn wrap_with_envelope(
97    response: Value,
98    method: &str,
99    command: &str,
100    profile_name: Option<String>,
101) -> Result<CommandResponse, String> {
102    let profile_name_str = profile_name.unwrap_or_else(|| "default".to_string());
103    let config_path = default_config_path().map_err(|e| e.to_string())?;
104    let profile = resolve_profile_full(&config_path, &profile_name_str)
105        .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name_str, e))?;
106
107    Ok(CommandResponse::new(
108        response,
109        Some(profile_name_str),
110        profile.team_id,
111        profile.user_id,
112        method.to_string(),
113        command.to_string(),
114    ))
115}
116
117/// Get option value from args (e.g., --key=value)
118pub fn get_option(args: &[String], prefix: &str) -> Option<String> {
119    args.iter()
120        .find(|arg| arg.starts_with(prefix))
121        .and_then(|arg| arg.strip_prefix(prefix))
122        .map(|s| s.to_string())
123}
124
125/// Parse token type from command line arguments
126/// Supports both --token-type=VALUE and --token-type VALUE formats
127pub fn parse_token_type(args: &[String]) -> Result<Option<TokenType>, String> {
128    // First try --token-type=VALUE format
129    if let Some(token_type_str) = get_option(args, "--token-type=") {
130        return token_type_str
131            .parse::<TokenType>()
132            .map(Some)
133            .map_err(|e| e.to_string());
134    }
135
136    // Then try --token-type VALUE format (space-separated)
137    if let Some(pos) = args.iter().position(|arg| arg == "--token-type") {
138        if let Some(value) = args.get(pos + 1) {
139            return value
140                .parse::<TokenType>()
141                .map(Some)
142                .map_err(|e| e.to_string());
143        } else {
144            return Err("--token-type requires a value (bot or user)".to_string());
145        }
146    }
147
148    Ok(None)
149}
150
151pub async fn run_search(args: &[String]) -> Result<(), String> {
152    let query = args[2].clone();
153    let count = get_option(args, "--count=").and_then(|s| s.parse().ok());
154    let page = get_option(args, "--page=").and_then(|s| s.parse().ok());
155    let sort = get_option(args, "--sort=");
156    let sort_dir = get_option(args, "--sort_dir=");
157    let profile = get_option(args, "--profile=");
158    let token_type = parse_token_type(args)?;
159    let raw = has_flag(args, "--raw");
160
161    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
162    let response = commands::search(&client, query, count, page, sort, sort_dir)
163        .await
164        .map_err(|e| e.to_string())?;
165
166    // Output with or without envelope
167    let output = if raw {
168        serde_json::to_string_pretty(&response).unwrap()
169    } else {
170        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
171        let wrapped =
172            wrap_with_envelope(response_value, "search.messages", "search", profile).await?;
173        serde_json::to_string_pretty(&wrapped).unwrap()
174    };
175
176    println!("{}", output);
177    Ok(())
178}
179
180/// Get all options with a specific prefix from args
181pub fn get_all_options(args: &[String], prefix: &str) -> Vec<String> {
182    args.iter()
183        .filter(|arg| arg.starts_with(prefix))
184        .filter_map(|arg| arg.strip_prefix(prefix))
185        .map(|s| s.to_string())
186        .collect()
187}
188
189pub async fn run_conv_list(args: &[String]) -> Result<(), String> {
190    let types = get_option(args, "--types=");
191    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
192    let profile = get_option(args, "--profile=");
193    let token_type = parse_token_type(args)?;
194    let filter_strings = get_all_options(args, "--filter=");
195    let raw = has_flag(args, "--raw");
196
197    // Parse format option (default: json)
198    let format = if let Some(fmt_str) = get_option(args, "--format=") {
199        commands::OutputFormat::parse(&fmt_str)?
200    } else {
201        commands::OutputFormat::Json
202    };
203
204    // Validate --raw compatibility
205    if raw && format != commands::OutputFormat::Json {
206        return Err(format!(
207            "--raw is only valid with --format json, but got --format {}",
208            format
209        ));
210    }
211
212    // Parse sort options
213    let sort_key = if let Some(sort_str) = get_option(args, "--sort=") {
214        Some(commands::SortKey::parse(&sort_str)?)
215    } else {
216        None
217    };
218
219    let sort_dir = if let Some(dir_str) = get_option(args, "--sort-dir=") {
220        commands::SortDirection::parse(&dir_str)?
221    } else {
222        commands::SortDirection::default()
223    };
224
225    // Parse filters
226    let filters: Result<Vec<_>, _> = filter_strings
227        .iter()
228        .map(|s| commands::ConversationFilter::parse(s))
229        .collect();
230    let filters = filters.map_err(|e| e.to_string())?;
231
232    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
233    let mut response = commands::conv_list(&client, types, limit)
234        .await
235        .map_err(|e| e.to_string())?;
236
237    // Apply filters
238    commands::apply_filters(&mut response, &filters);
239
240    // Apply sorting if specified
241    if let Some(key) = sort_key {
242        commands::sort_conversations(&mut response, key, sort_dir);
243    }
244
245    // Format output: non-JSON formats bypass raw/envelope logic
246    let output = if format != commands::OutputFormat::Json {
247        commands::format_response(&response, format)?
248    } else if raw {
249        serde_json::to_string_pretty(&response).unwrap()
250    } else {
251        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
252        let wrapped =
253            wrap_with_envelope(response_value, "conversations.list", "conv list", profile).await?;
254        serde_json::to_string_pretty(&wrapped).unwrap()
255    };
256
257    println!("{}", output);
258    Ok(())
259}
260
261pub async fn run_conv_select(args: &[String]) -> Result<(), String> {
262    let types = get_option(args, "--types=");
263    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
264    let profile = get_option(args, "--profile=");
265    let token_type = parse_token_type(args)?;
266    let filter_strings = get_all_options(args, "--filter=");
267
268    // Parse filters
269    let filters: Result<Vec<_>, _> = filter_strings
270        .iter()
271        .map(|s| commands::ConversationFilter::parse(s))
272        .collect();
273    let filters = filters.map_err(|e| e.to_string())?;
274
275    let client = get_api_client_with_token_type(profile, token_type).await?;
276    let mut response = commands::conv_list(&client, types, limit)
277        .await
278        .map_err(|e| e.to_string())?;
279
280    // Apply filters
281    commands::apply_filters(&mut response, &filters);
282
283    // Extract conversations and present selection
284    let items = commands::extract_conversations(&response);
285    let selector = commands::StdinSelector;
286    let channel_id = selector.select(&items)?;
287
288    println!("{}", channel_id);
289    Ok(())
290}
291
292pub async fn run_conv_history(args: &[String]) -> Result<(), String> {
293    let interactive = has_flag(args, "--interactive");
294
295    let channel = if interactive {
296        // Use conv_select logic to get channel
297        let types = get_option(args, "--types=");
298        let profile = get_option(args, "--profile=");
299        let filter_strings = get_all_options(args, "--filter=");
300
301        // Parse filters
302        let filters: Result<Vec<_>, _> = filter_strings
303            .iter()
304            .map(|s| commands::ConversationFilter::parse(s))
305            .collect();
306        let filters = filters.map_err(|e| e.to_string())?;
307
308        let token_type_inner = parse_token_type(args)?;
309        let client = get_api_client_with_token_type(profile.clone(), token_type_inner).await?;
310        let mut response = commands::conv_list(&client, types, None)
311            .await
312            .map_err(|e| e.to_string())?;
313
314        // Apply filters
315        commands::apply_filters(&mut response, &filters);
316
317        // Extract conversations and present selection
318        let items = commands::extract_conversations(&response);
319        let selector = commands::StdinSelector;
320        selector.select(&items)?
321    } else {
322        if args.len() < 4 {
323            return Err("Channel argument required when --interactive is not used".to_string());
324        }
325        args[3].clone()
326    };
327
328    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
329    let oldest = get_option(args, "--oldest=");
330    let latest = get_option(args, "--latest=");
331    let profile = get_option(args, "--profile=");
332    let token_type = parse_token_type(args)?;
333    let raw = has_flag(args, "--raw");
334
335    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
336    let response = commands::conv_history(&client, channel, limit, oldest, latest)
337        .await
338        .map_err(|e| e.to_string())?;
339
340    // Output with or without envelope
341    let output = if raw {
342        serde_json::to_string_pretty(&response).unwrap()
343    } else {
344        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
345        let wrapped = wrap_with_envelope(
346            response_value,
347            "conversations.history",
348            "conv history",
349            profile,
350        )
351        .await?;
352        serde_json::to_string_pretty(&wrapped).unwrap()
353    };
354
355    println!("{}", output);
356    Ok(())
357}
358
359pub async fn run_users_info(args: &[String]) -> Result<(), String> {
360    let user = args[3].clone();
361    let profile = get_option(args, "--profile=");
362    let token_type = parse_token_type(args)?;
363    let raw = has_flag(args, "--raw");
364
365    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
366    let response = commands::users_info(&client, user)
367        .await
368        .map_err(|e| e.to_string())?;
369
370    // Output with or without envelope
371    let output = if raw {
372        serde_json::to_string_pretty(&response).unwrap()
373    } else {
374        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
375        let wrapped =
376            wrap_with_envelope(response_value, "users.info", "users info", profile).await?;
377        serde_json::to_string_pretty(&wrapped).unwrap()
378    };
379
380    println!("{}", output);
381    Ok(())
382}
383
384pub async fn run_users_cache_update(args: &[String]) -> Result<(), String> {
385    let profile_name = get_option(args, "--profile=").unwrap_or_else(|| "default".to_string());
386    let force = has_flag(args, "--force");
387    let token_type = parse_token_type(args)?;
388
389    let config_path = default_config_path().map_err(|e| e.to_string())?;
390    let config = load_config(&config_path).map_err(|e| e.to_string())?;
391
392    let profile = config
393        .get(&profile_name)
394        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
395
396    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
397
398    commands::update_cache(&client, profile.team_id.clone(), force)
399        .await
400        .map_err(|e| e.to_string())?;
401
402    println!("Cache updated successfully for team {}", profile.team_id);
403    Ok(())
404}
405
406pub async fn run_users_resolve_mentions(args: &[String]) -> Result<(), String> {
407    if args.len() < 4 {
408        return Err(
409            "Usage: users resolve-mentions <text> [--profile=NAME] [--format=FORMAT]".to_string(),
410        );
411    }
412
413    let text = args[3].clone();
414    let profile_name = get_option(args, "--profile=").unwrap_or_else(|| "default".to_string());
415    let format_str = get_option(args, "--format=").unwrap_or_else(|| "display_name".to_string());
416
417    let format = format_str.parse::<commands::MentionFormat>().map_err(|_| {
418        format!(
419            "Invalid format: {}. Use display_name, real_name, or username",
420            format_str
421        )
422    })?;
423
424    let config_path = default_config_path().map_err(|e| e.to_string())?;
425    let config = load_config(&config_path).map_err(|e| e.to_string())?;
426
427    let profile = config
428        .get(&profile_name)
429        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
430
431    let cache_path = commands::UsersCacheFile::default_path()?;
432    let cache_file = commands::UsersCacheFile::load(&cache_path)?;
433
434    let workspace_cache = cache_file.get_workspace(&profile.team_id).ok_or_else(|| {
435        format!(
436            "No cache found for team {}. Run 'users cache-update' first.",
437            profile.team_id
438        )
439    })?;
440
441    let result = commands::resolve_mentions(&text, workspace_cache, format);
442    println!("{}", result);
443    Ok(())
444}
445
446pub async fn run_msg_post(args: &[String]) -> Result<(), String> {
447    if args.len() < 5 {
448        return Err("Usage: msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--profile=NAME] [--token-type=bot|user]".to_string());
449    }
450
451    let channel = args[3].clone();
452    let text = args[4].clone();
453    let thread_ts = get_option(args, "--thread-ts=");
454    let reply_broadcast = has_flag(args, "--reply-broadcast");
455    let profile = get_option(args, "--profile=");
456    let token_type = parse_token_type(args)?;
457
458    // Validate: --reply-broadcast requires --thread-ts
459    if reply_broadcast && thread_ts.is_none() {
460        return Err("Error: --reply-broadcast requires --thread-ts".to_string());
461    }
462
463    let raw = has_flag(args, "--raw");
464    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
465    let response = commands::msg_post(&client, channel, text, thread_ts, reply_broadcast)
466        .await
467        .map_err(|e| e.to_string())?;
468
469    // Output with or without envelope
470    let output = if raw {
471        serde_json::to_string_pretty(&response).unwrap()
472    } else {
473        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
474        let wrapped =
475            wrap_with_envelope(response_value, "chat.postMessage", "msg post", profile).await?;
476        serde_json::to_string_pretty(&wrapped).unwrap()
477    };
478
479    println!("{}", output);
480    Ok(())
481}
482
483pub async fn run_msg_update(args: &[String], non_interactive: bool) -> Result<(), String> {
484    if args.len() < 6 {
485        return Err("Usage: msg update <channel> <ts> <text> [--yes] [--profile=NAME] [--token-type=bot|user]".to_string());
486    }
487
488    let channel = args[3].clone();
489    let ts = args[4].clone();
490    let text = args[5].clone();
491    let yes = has_flag(args, "--yes");
492    let profile = get_option(args, "--profile=");
493    let token_type = parse_token_type(args)?;
494    let raw = has_flag(args, "--raw");
495
496    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
497    let response = commands::msg_update(&client, channel, ts, text, yes, non_interactive)
498        .await
499        .map_err(|e| e.to_string())?;
500
501    // Output with or without envelope
502    let output = if raw {
503        serde_json::to_string_pretty(&response).unwrap()
504    } else {
505        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
506        let wrapped =
507            wrap_with_envelope(response_value, "chat.update", "msg update", profile).await?;
508        serde_json::to_string_pretty(&wrapped).unwrap()
509    };
510
511    println!("{}", output);
512    Ok(())
513}
514
515pub async fn run_msg_delete(args: &[String], non_interactive: bool) -> Result<(), String> {
516    if args.len() < 5 {
517        return Err(
518            "Usage: msg delete <channel> <ts> [--yes] [--profile=NAME] [--token-type=bot|user]"
519                .to_string(),
520        );
521    }
522
523    let channel = args[3].clone();
524    let ts = args[4].clone();
525    let yes = has_flag(args, "--yes");
526    let profile = get_option(args, "--profile=");
527    let token_type = parse_token_type(args)?;
528    let raw = has_flag(args, "--raw");
529
530    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
531    let response = commands::msg_delete(&client, channel, ts, yes, non_interactive)
532        .await
533        .map_err(|e| e.to_string())?;
534
535    // Output with or without envelope
536    let output = if raw {
537        serde_json::to_string_pretty(&response).unwrap()
538    } else {
539        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
540        let wrapped =
541            wrap_with_envelope(response_value, "chat.delete", "msg delete", profile).await?;
542        serde_json::to_string_pretty(&wrapped).unwrap()
543    };
544
545    println!("{}", output);
546    Ok(())
547}
548
549pub async fn run_react_add(args: &[String]) -> Result<(), String> {
550    if args.len() < 6 {
551        return Err(
552            "Usage: react add <channel> <ts> <emoji> [--profile=NAME] [--token-type=bot|user]"
553                .to_string(),
554        );
555    }
556
557    let channel = args[3].clone();
558    let ts = args[4].clone();
559    let emoji = args[5].clone();
560    let profile = get_option(args, "--profile=");
561    let token_type = parse_token_type(args)?;
562    let raw = has_flag(args, "--raw");
563
564    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
565    let response = commands::react_add(&client, channel, ts, emoji)
566        .await
567        .map_err(|e| e.to_string())?;
568
569    // Output with or without envelope
570    let output = if raw {
571        serde_json::to_string_pretty(&response).unwrap()
572    } else {
573        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
574        let wrapped =
575            wrap_with_envelope(response_value, "reactions.add", "react add", profile).await?;
576        serde_json::to_string_pretty(&wrapped).unwrap()
577    };
578
579    println!("{}", output);
580    Ok(())
581}
582
583pub async fn run_react_remove(args: &[String], non_interactive: bool) -> Result<(), String> {
584    if args.len() < 6 {
585        return Err(
586            "Usage: react remove <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user]".to_string(),
587        );
588    }
589
590    let channel = args[3].clone();
591    let ts = args[4].clone();
592    let emoji = args[5].clone();
593    let yes = has_flag(args, "--yes");
594    let profile = get_option(args, "--profile=");
595    let token_type = parse_token_type(args)?;
596    let raw = has_flag(args, "--raw");
597
598    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
599    let response = commands::react_remove(&client, channel, ts, emoji, yes, non_interactive)
600        .await
601        .map_err(|e| e.to_string())?;
602
603    // Output with or without envelope
604    let output = if raw {
605        serde_json::to_string_pretty(&response).unwrap()
606    } else {
607        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
608        let wrapped =
609            wrap_with_envelope(response_value, "reactions.remove", "react remove", profile).await?;
610        serde_json::to_string_pretty(&wrapped).unwrap()
611    };
612
613    println!("{}", output);
614    Ok(())
615}
616
617pub async fn run_file_upload(args: &[String]) -> Result<(), String> {
618    if args.len() < 4 {
619        return Err(
620            "Usage: file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--profile=NAME] [--token-type=bot|user]"
621                .to_string(),
622        );
623    }
624
625    let file_path = args[3].clone();
626
627    // Support both --channel and --channels
628    let channels = get_option(args, "--channel=").or_else(|| get_option(args, "--channels="));
629    let title = get_option(args, "--title=");
630    let comment = get_option(args, "--comment=");
631    let profile = get_option(args, "--profile=");
632    let token_type = parse_token_type(args)?;
633    let raw = has_flag(args, "--raw");
634
635    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
636    let response = commands::file_upload(&client, file_path, channels, title, comment)
637        .await
638        .map_err(|e| e.to_string())?;
639
640    // Output with or without envelope
641    let output = if raw {
642        serde_json::to_string_pretty(&response).unwrap()
643    } else {
644        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
645        let wrapped =
646            wrap_with_envelope(response_value, "files.upload", "file upload", profile).await?;
647        serde_json::to_string_pretty(&wrapped).unwrap()
648    };
649
650    println!("{}", output);
651    Ok(())
652}
653
654pub fn print_conv_usage(prog: &str) {
655    println!("Conv command usage:");
656    println!(
657        "  {} conv list [--types=TYPE] [--limit=N] [--filter=KEY:VALUE]... [--format=FORMAT] [--sort=KEY] [--sort-dir=DIR] [--raw] [--profile=NAME] [--token-type=bot|user]",
658        prog
659    );
660    println!("    Filters: name:<glob>, is_member:true|false, is_private:true|false");
661    println!("    Formats: json (default), jsonl, table, tsv");
662    println!("    Sort keys: name, created, num_members");
663    println!("    Sort direction: asc (default), desc");
664    println!("    Note: --raw is only valid with --format json");
665    println!(
666        "  {} conv select [--types=TYPE] [--filter=KEY:VALUE]... [--profile=NAME]",
667        prog
668    );
669    println!("    Interactively select a conversation and output its channel ID");
670    println!(
671        "  {} conv history <channel> [--limit=N] [--oldest=TS] [--latest=TS] [--profile=NAME] [--token-type=bot|user]",
672        prog
673    );
674    println!(
675        "  {} conv history --interactive [--types=TYPE] [--filter=KEY:VALUE]... [--limit=N] [--profile=NAME]",
676        prog
677    );
678    println!("    Select channel interactively before fetching history");
679}
680
681pub fn print_users_usage(prog: &str) {
682    println!("Users command usage:");
683    println!(
684        "  {} users info <user_id> [--profile=NAME] [--token-type=bot|user]",
685        prog
686    );
687    println!(
688        "  {} users cache-update [--profile=NAME] [--force] [--token-type=bot|user]",
689        prog
690    );
691    println!("  {} users resolve-mentions <text> [--profile=NAME] [--format=display_name|real_name|username]", prog);
692}
693
694pub fn print_msg_usage(prog: &str) {
695    println!("Msg command usage:");
696    println!(
697        "  {} msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--profile=NAME] [--token-type=bot|user]",
698        prog
699    );
700    println!(
701        "  {} msg update <channel> <ts> <text> [--yes] [--profile=NAME] [--token-type=bot|user]",
702        prog
703    );
704    println!(
705        "  {} msg delete <channel> <ts> [--yes] [--profile=NAME] [--token-type=bot|user]",
706        prog
707    );
708}
709
710pub fn print_react_usage(prog: &str) {
711    println!("React command usage:");
712    println!(
713        "  {} react add <channel> <ts> <emoji> [--profile=NAME] [--token-type=bot|user]",
714        prog
715    );
716    println!(
717        "  {} react remove <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user]",
718        prog
719    );
720}
721
722pub fn print_file_usage(prog: &str) {
723    println!("File command usage:");
724    println!(
725        "  {} file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--profile=NAME] [--token-type=bot|user]",
726        prog
727    );
728}
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733
734    #[test]
735    fn test_parse_token_type_equals_format() {
736        let args = vec!["command".to_string(), "--token-type=user".to_string()];
737        let result = parse_token_type(&args).unwrap();
738        assert_eq!(result, Some(TokenType::User));
739    }
740
741    #[test]
742    fn test_parse_token_type_space_separated() {
743        let args = vec![
744            "command".to_string(),
745            "--token-type".to_string(),
746            "bot".to_string(),
747        ];
748        let result = parse_token_type(&args).unwrap();
749        assert_eq!(result, Some(TokenType::Bot));
750    }
751
752    #[test]
753    fn test_parse_token_type_both_values() {
754        // Test user with equals
755        let args1 = vec!["--token-type=user".to_string()];
756        assert_eq!(parse_token_type(&args1).unwrap(), Some(TokenType::User));
757
758        // Test bot with equals
759        let args2 = vec!["--token-type=bot".to_string()];
760        assert_eq!(parse_token_type(&args2).unwrap(), Some(TokenType::Bot));
761
762        // Test user with space
763        let args3 = vec!["--token-type".to_string(), "user".to_string()];
764        assert_eq!(parse_token_type(&args3).unwrap(), Some(TokenType::User));
765
766        // Test bot with space
767        let args4 = vec!["--token-type".to_string(), "bot".to_string()];
768        assert_eq!(parse_token_type(&args4).unwrap(), Some(TokenType::Bot));
769    }
770
771    #[test]
772    fn test_parse_token_type_missing() {
773        let args = vec!["command".to_string()];
774        let result = parse_token_type(&args).unwrap();
775        assert_eq!(result, None);
776    }
777
778    #[test]
779    fn test_parse_token_type_missing_value() {
780        let args = vec!["--token-type".to_string()];
781        let result = parse_token_type(&args);
782        assert!(result.is_err());
783        assert_eq!(
784            result.unwrap_err(),
785            "--token-type requires a value (bot or user)"
786        );
787    }
788
789    #[test]
790    fn test_parse_token_type_invalid_value() {
791        let args = vec!["--token-type=invalid".to_string()];
792        let result = parse_token_type(&args);
793        assert!(result.is_err());
794    }
795}