1mod context;
4mod handlers;
5mod help;
6pub mod introspection;
7
8pub use context::CliContext;
9pub use handlers::{
10 handle_export_command, handle_import_command, run_api_call, run_auth_login, run_install_skill,
11};
12pub use introspection::{
13 generate_commands_list, generate_help, generate_schema, CommandDef, CommandsListResponse,
14 HelpResponse, SchemaResponse,
15};
16
17use crate::api::{ApiClient, CommandResponse};
18use crate::commands;
19use crate::commands::ConversationSelector;
20use crate::debug;
21use crate::profile::{
22 create_token_store, default_config_path, load_config, make_token_key, resolve_profile_full,
23 TokenStore, TokenType,
24};
25use serde_json::Value;
26
27#[allow(dead_code)]
46pub fn resolve_token_for_wrapper(
47 slack_token_env: Option<String>,
48 token_store: &dyn TokenStore,
49 token_key: &str,
50 fallback_token_key: Option<&str>,
51 explicit_request: bool,
52) -> Result<String, String> {
53 if let Some(env_token) = slack_token_env {
55 return Ok(env_token);
56 }
57
58 if let Ok(token) = token_store.get(token_key) {
60 return Ok(token);
61 }
62
63 if !explicit_request {
65 if let Some(fallback_key) = fallback_token_key {
66 if let Ok(token) = token_store.get(fallback_key) {
67 eprintln!("Warning: Primary token not found, falling back to alternative token");
68 return Ok(token);
69 }
70 }
71 }
72
73 if explicit_request {
75 Err(
76 "No token found for explicitly requested token type. Set SLACK_TOKEN environment variable or run 'slack login' to obtain a token.".to_string()
77 )
78 } else {
79 Err(
80 "No token found. Set SLACK_TOKEN environment variable or run 'slack login' to obtain a token.".to_string()
81 )
82 }
83}
84
85pub async fn get_api_client_with_token_type(
97 profile_name: Option<String>,
98 token_type: Option<TokenType>,
99) -> Result<ApiClient, String> {
100 if let Ok(env_token) = std::env::var("SLACK_TOKEN") {
102 return Ok(ApiClient::with_token(env_token));
103 }
104
105 let profile_name = profile_name.unwrap_or_else(|| "default".to_string());
106 let config_path = default_config_path().map_err(|e| e.to_string())?;
107 let config = load_config(&config_path).map_err(|e| e.to_string())?;
108
109 let profile = config
110 .get(&profile_name)
111 .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
112
113 let token_store = create_token_store().map_err(|e| e.to_string())?;
114
115 let resolved_token_type = token_type.or(profile.default_token_type);
117
118 let bot_token_key = make_token_key(&profile.team_id, &profile.user_id);
119 let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
120
121 let token = match resolved_token_type {
122 Some(TokenType::Bot) => {
123 token_store
125 .get(&bot_token_key)
126 .map_err(|e| format!("Failed to get bot token: {}", e))?
127 }
128 Some(TokenType::User) => {
129 token_store
131 .get(&user_token_key)
132 .map_err(|e| format!("Failed to get user token: {}", e))?
133 }
134 None => {
135 match token_store.get(&user_token_key) {
137 Ok(user_token) => user_token,
138 Err(_) => {
139 token_store
141 .get(&bot_token_key)
142 .map_err(|e| format!("Failed to get token: {}", e))?
143 }
144 }
145 }
146 };
147
148 Ok(ApiClient::with_token(token))
149}
150
151#[allow(dead_code)]
153pub async fn get_api_client(profile_name: Option<String>) -> Result<ApiClient, String> {
154 get_api_client_with_token_type(profile_name, None).await
155}
156
157pub fn has_flag(args: &[String], flag: &str) -> bool {
159 args.iter().any(|arg| arg == flag)
160}
161
162pub fn should_output_raw(args: &[String]) -> bool {
176 if has_flag(args, "--raw") {
178 return true;
179 }
180
181 if let Ok(output_mode) = std::env::var("SLACKRS_OUTPUT") {
183 return output_mode.trim().to_lowercase() == "raw";
184 }
185
186 false
188}
189
190pub fn is_non_interactive_error(error_msg: &str) -> bool {
192 error_msg.contains("Non-interactive mode error")
193 || error_msg.contains("Use --yes flag to confirm in non-interactive mode")
194}
195
196#[allow(dead_code)]
198pub async fn wrap_with_envelope(
199 response: Value,
200 method: &str,
201 command: &str,
202 profile_name: Option<String>,
203) -> Result<CommandResponse, String> {
204 wrap_with_envelope_and_token_type(response, method, command, profile_name, None).await
205}
206
207pub async fn wrap_with_envelope_and_token_type(
209 response: Value,
210 method: &str,
211 command: &str,
212 profile_name: Option<String>,
213 explicit_token_type: Option<TokenType>,
214) -> Result<CommandResponse, String> {
215 let profile_name_str = profile_name.unwrap_or_else(|| "default".to_string());
216 let config_path = default_config_path().map_err(|e| e.to_string())?;
217 let profile = resolve_profile_full(&config_path, &profile_name_str)
218 .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name_str, e))?;
219
220 let token_type_str = if let Some(explicit) = explicit_token_type {
222 Some(explicit.to_string())
224 } else if std::env::var("SLACK_TOKEN").is_ok() {
225 Some(
227 profile
228 .default_token_type
229 .map(|t| t.to_string())
230 .unwrap_or_else(|| "bot".to_string()),
231 )
232 } else {
233 let token_store = create_token_store().map_err(|e| e.to_string())?;
235 let bot_token_key = make_token_key(&profile.team_id, &profile.user_id);
236 let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
237
238 let resolved_type = profile.default_token_type.or_else(|| {
240 if token_store.get(&user_token_key).is_ok() {
242 Some(TokenType::User)
243 } else if token_store.get(&bot_token_key).is_ok() {
244 Some(TokenType::Bot)
245 } else {
246 None
247 }
248 });
249
250 resolved_type.map(|t| t.to_string())
251 };
252
253 Ok(CommandResponse::with_token_type(
254 response,
255 Some(profile_name_str),
256 profile.team_id,
257 profile.user_id,
258 method.to_string(),
259 command.to_string(),
260 token_type_str,
261 ))
262}
263
264pub fn resolve_profile_name(args: &[String]) -> String {
281 if let Some(profile) = get_option(args, "--profile=") {
283 return profile;
284 }
285
286 if let Ok(profile) = std::env::var("SLACK_PROFILE") {
288 return profile;
289 }
290
291 "default".to_string()
293}
294
295pub fn get_option(args: &[String], prefix: &str) -> Option<String> {
299 if let Some(value) = args
301 .iter()
302 .find(|arg| arg.starts_with(prefix))
303 .and_then(|arg| arg.strip_prefix(prefix))
304 .map(|s| s.to_string())
305 {
306 return Some(value);
307 }
308
309 let flag = prefix.strip_suffix('=').unwrap_or(prefix);
312 if let Some(pos) = args.iter().position(|arg| arg == flag) {
313 if let Some(value) = args.get(pos + 1) {
314 if !value.starts_with('-') {
316 return Some(value.clone());
317 }
318 }
319 }
320
321 None
322}
323
324pub fn parse_token_type(args: &[String]) -> Result<Option<TokenType>, String> {
327 if let Some(token_type_str) = get_option(args, "--token-type=") {
329 return token_type_str
330 .parse::<TokenType>()
331 .map(Some)
332 .map_err(|e| e.to_string());
333 }
334
335 if let Some(pos) = args.iter().position(|arg| arg == "--token-type") {
337 if let Some(value) = args.get(pos + 1) {
338 return value
339 .parse::<TokenType>()
340 .map(Some)
341 .map_err(|e| e.to_string());
342 } else {
343 return Err("--token-type requires a value (bot or user)".to_string());
344 }
345 }
346
347 Ok(None)
348}
349
350pub async fn run_search(args: &[String]) -> Result<(), String> {
351 let query = args[2].clone();
352 let count = get_option(args, "--count=").and_then(|s| s.parse().ok());
353 let page = get_option(args, "--page=").and_then(|s| s.parse().ok());
354 let sort = get_option(args, "--sort=");
355 let sort_dir = get_option(args, "--sort_dir=");
356 let profile_name = resolve_profile_name(args);
357 let token_type = parse_token_type(args)?;
358 let raw = should_output_raw(args);
359
360 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
361 let response = commands::search(&client, query, count, page, sort, sort_dir)
362 .await
363 .map_err(|e| e.to_string())?;
364
365 crate::api::display_wrapper_error_guidance(&response);
367
368 let output = if raw {
370 serde_json::to_string_pretty(&response).unwrap()
371 } else {
372 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
373 let wrapped = wrap_with_envelope_and_token_type(
374 response_value,
375 "search.messages",
376 "search",
377 Some(profile_name),
378 token_type,
379 )
380 .await?;
381 serde_json::to_string_pretty(&wrapped).unwrap()
382 };
383
384 println!("{}", output);
385 Ok(())
386}
387
388pub fn get_all_options(args: &[String], prefix: &str) -> Vec<String> {
392 let mut results = Vec::new();
393
394 results.extend(
396 args.iter()
397 .filter(|arg| arg.starts_with(prefix))
398 .filter_map(|arg| arg.strip_prefix(prefix))
399 .map(|s| s.to_string()),
400 );
401
402 let flag = prefix.strip_suffix('=').unwrap_or(prefix);
404 let mut i = 0;
405 while i < args.len() {
406 if args[i] == flag {
407 if let Some(value) = args.get(i + 1) {
408 if !value.starts_with('-') {
410 results.push(value.clone());
411 i += 2; continue;
413 }
414 }
415 }
416 i += 1;
417 }
418
419 results
420}
421
422pub async fn run_conv_list(args: &[String]) -> Result<(), String> {
423 if has_flag(args, "--help") || has_flag(args, "-h") {
425 print_conv_usage(&args[0]);
426 return Ok(());
427 }
428
429 let types = get_option(args, "--types=");
430 let include_private = has_flag(args, "--include-private");
431 let all = has_flag(args, "--all");
432 let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
433 let profile_name = resolve_profile_name(args);
434 let token_type = parse_token_type(args)?;
435 let filter_strings = get_all_options(args, "--filter=");
436 let raw = should_output_raw(args);
437
438 if types.is_some() && (include_private || all) {
440 return Err("Error: --types cannot be used with --include-private or --all".to_string());
441 }
442
443 let resolved_types = if let Some(explicit_types) = types {
445 Some(explicit_types)
447 } else if all {
448 Some("public_channel,private_channel,im,mpim".to_string())
450 } else if include_private {
451 Some("public_channel,private_channel".to_string())
453 } else {
454 Some("public_channel,private_channel".to_string())
456 };
457
458 let format = if let Some(fmt_str) = get_option(args, "--format=") {
460 commands::OutputFormat::parse(&fmt_str)?
461 } else {
462 commands::OutputFormat::Json
463 };
464
465 if raw && format != commands::OutputFormat::Json {
467 return Err(format!(
468 "--raw is only valid with --format json, but got --format {}",
469 format
470 ));
471 }
472
473 let sort_key = if let Some(sort_str) = get_option(args, "--sort=") {
475 Some(commands::SortKey::parse(&sort_str)?)
476 } else {
477 None
478 };
479
480 let sort_dir = if let Some(dir_str) = get_option(args, "--sort-dir=") {
481 commands::SortDirection::parse(&dir_str)?
482 } else {
483 commands::SortDirection::default()
484 };
485
486 let filters: Result<Vec<_>, _> = filter_strings
488 .iter()
489 .map(|s| commands::ConversationFilter::parse(s))
490 .collect();
491 let filters = filters.map_err(|e| e.to_string())?;
492
493 let debug_level = debug::get_debug_level(args);
495
496 let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
498 "environment"
499 } else {
500 "file"
501 };
502
503 let resolved_token_type = if let Some(explicit) = token_type {
505 explicit
506 } else {
507 let config_path = default_config_path().map_err(|e| e.to_string())?;
509 let profile = resolve_profile_full(&config_path, &profile_name)
510 .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
511
512 if let Some(default_type) = profile.default_token_type {
513 default_type
514 } else {
515 let token_store = create_token_store().map_err(|e| e.to_string())?;
517 let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
518 if token_store.get(&user_token_key).is_ok() {
519 TokenType::User
520 } else {
521 TokenType::Bot
522 }
523 }
524 };
525
526 let endpoint = "https://slack.com/api/conversations.list";
527
528 debug::log_api_context(
529 debug_level,
530 Some(&profile_name),
531 token_store_backend,
532 resolved_token_type.as_str(),
533 "conversations.list",
534 endpoint,
535 );
536
537 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
538 let mut response = commands::conv_list(&client, resolved_types, limit)
539 .await
540 .map_err(|e| e.to_string())?;
541
542 debug::log_error_code(
544 debug_level,
545 &serde_json::to_value(&response).unwrap_or_default(),
546 );
547
548 crate::api::display_wrapper_error_guidance(&response);
550
551 commands::apply_filters(&mut response, &filters);
553
554 if let Some(key) = sort_key {
556 commands::sort_conversations(&mut response, key, sort_dir);
557 }
558
559 let output = if format != commands::OutputFormat::Json {
561 commands::format_response(&response, format)?
562 } else if raw {
563 serde_json::to_string_pretty(&response).unwrap()
564 } else {
565 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
566 let wrapped = wrap_with_envelope_and_token_type(
567 response_value,
568 "conversations.list",
569 "conv list",
570 Some(profile_name),
571 token_type,
572 )
573 .await?;
574 serde_json::to_string_pretty(&wrapped).unwrap()
575 };
576
577 println!("{}", output);
578 Ok(())
579}
580
581pub async fn run_conv_select(args: &[String]) -> Result<(), String> {
582 if has_flag(args, "--help") || has_flag(args, "-h") {
584 print_conv_usage(&args[0]);
585 return Ok(());
586 }
587
588 let types = get_option(args, "--types=");
589 let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
590 let profile_name = resolve_profile_name(args);
591 let token_type = parse_token_type(args)?;
592 let filter_strings = get_all_options(args, "--filter=");
593
594 let filters: Result<Vec<_>, _> = filter_strings
596 .iter()
597 .map(|s| commands::ConversationFilter::parse(s))
598 .collect();
599 let filters = filters.map_err(|e| e.to_string())?;
600
601 let resolved_types = types.or(Some("public_channel,private_channel".to_string()));
603
604 let client = get_api_client_with_token_type(Some(profile_name), token_type).await?;
605 let mut response = commands::conv_list(&client, resolved_types, limit)
606 .await
607 .map_err(|e| e.to_string())?;
608
609 commands::apply_filters(&mut response, &filters);
611
612 let items = commands::extract_conversations(&response);
614 let selector = commands::StdinSelector;
615 let channel_id = selector.select(&items)?;
616
617 println!("{}", channel_id);
618 Ok(())
619}
620
621pub async fn run_conv_search(args: &[String]) -> Result<(), String> {
622 if has_flag(args, "--help") || has_flag(args, "-h") {
624 print_conv_usage(&args[0]);
625 return Ok(());
626 }
627
628 let pattern = args
630 .get(3)
631 .filter(|arg| !arg.starts_with("--"))
632 .ok_or_else(|| "Search pattern is required".to_string())?
633 .clone();
634
635 let types = get_option(args, "--types=");
636 let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
637 let profile_name = resolve_profile_name(args);
638 let token_type = parse_token_type(args)?;
639 let raw = should_output_raw(args);
640 let select = has_flag(args, "--select");
641
642 let filter_strings = get_all_options(args, "--filter=");
644
645 let format = if let Some(fmt_str) = get_option(args, "--format=") {
647 commands::OutputFormat::parse(&fmt_str)?
648 } else {
649 commands::OutputFormat::Json
650 };
651
652 if raw && format != commands::OutputFormat::Json {
654 return Err(format!(
655 "--raw is only valid with --format json, but got --format {}",
656 format
657 ));
658 }
659
660 let sort_key = if let Some(sort_str) = get_option(args, "--sort=") {
662 Some(commands::SortKey::parse(&sort_str)?)
663 } else {
664 None
665 };
666
667 let sort_dir = if let Some(dir_str) = get_option(args, "--sort-dir=") {
668 commands::SortDirection::parse(&dir_str)?
669 } else {
670 commands::SortDirection::default()
671 };
672
673 let mut filters: Vec<commands::ConversationFilter> =
675 vec![commands::ConversationFilter::Name(pattern)];
676
677 for filter_str in filter_strings {
679 filters.push(commands::ConversationFilter::parse(&filter_str).map_err(|e| e.to_string())?);
680 }
681
682 let resolved_types = types.or(Some("public_channel,private_channel".to_string()));
684
685 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
686 let mut response = commands::conv_list(&client, resolved_types, limit)
687 .await
688 .map_err(|e| e.to_string())?;
689
690 commands::apply_filters(&mut response, &filters);
692
693 if let Some(key) = sort_key {
695 commands::sort_conversations(&mut response, key, sort_dir);
696 }
697
698 if select {
700 let items = commands::extract_conversations(&response);
701 let selector = commands::StdinSelector;
702 let channel_id = selector.select(&items)?;
703 println!("{}", channel_id);
704 return Ok(());
705 }
706
707 let output = if format != commands::OutputFormat::Json {
709 commands::format_response(&response, format)?
710 } else if raw {
711 serde_json::to_string_pretty(&response).unwrap()
712 } else {
713 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
714 let wrapped = wrap_with_envelope_and_token_type(
715 response_value,
716 "conversations.list",
717 "conv search",
718 Some(profile_name),
719 token_type,
720 )
721 .await?;
722 serde_json::to_string_pretty(&wrapped).unwrap()
723 };
724
725 println!("{}", output);
726 Ok(())
727}
728
729pub async fn run_conv_history(args: &[String]) -> Result<(), String> {
730 if has_flag(args, "--help") || has_flag(args, "-h") {
732 print_conv_usage(&args[0]);
733 return Ok(());
734 }
735
736 let interactive = has_flag(args, "--interactive");
737
738 let channel = if interactive {
739 let types = get_option(args, "--types=");
741 let profile_name_inner = resolve_profile_name(args);
742 let filter_strings = get_all_options(args, "--filter=");
743
744 let filters: Result<Vec<_>, _> = filter_strings
746 .iter()
747 .map(|s| commands::ConversationFilter::parse(s))
748 .collect();
749 let filters = filters.map_err(|e| e.to_string())?;
750
751 let resolved_types = types.or(Some("public_channel,private_channel".to_string()));
753
754 let token_type_inner = parse_token_type(args)?;
755 let client =
756 get_api_client_with_token_type(Some(profile_name_inner), token_type_inner).await?;
757 let mut response = commands::conv_list(&client, resolved_types, None)
758 .await
759 .map_err(|e| e.to_string())?;
760
761 commands::apply_filters(&mut response, &filters);
763
764 let items = commands::extract_conversations(&response);
766 let selector = commands::StdinSelector;
767 selector.select(&items)?
768 } else {
769 if args.len() < 4 {
770 return Err("Channel argument required when --interactive is not used".to_string());
771 }
772 args[3].clone()
773 };
774
775 let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
776 let oldest = get_option(args, "--oldest=");
777 let latest = get_option(args, "--latest=");
778 let profile_name = resolve_profile_name(args);
779 let token_type = parse_token_type(args)?;
780 let raw = should_output_raw(args);
781
782 let debug_level = debug::get_debug_level(args);
784
785 let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
787 "environment"
788 } else {
789 "file"
790 };
791
792 let resolved_token_type = if let Some(explicit) = token_type {
794 explicit
795 } else {
796 let config_path = default_config_path().map_err(|e| e.to_string())?;
797 let profile = resolve_profile_full(&config_path, &profile_name)
798 .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
799
800 if let Some(default_type) = profile.default_token_type {
801 default_type
802 } else {
803 let token_store = create_token_store().map_err(|e| e.to_string())?;
804 let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
805 if token_store.get(&user_token_key).is_ok() {
806 TokenType::User
807 } else {
808 TokenType::Bot
809 }
810 }
811 };
812
813 let endpoint = "https://slack.com/api/conversations.history";
814
815 debug::log_api_context(
816 debug_level,
817 Some(&profile_name),
818 token_store_backend,
819 resolved_token_type.as_str(),
820 "conversations.history",
821 endpoint,
822 );
823
824 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
825 let response = commands::conv_history(&client, channel, limit, oldest, latest)
826 .await
827 .map_err(|e| e.to_string())?;
828
829 debug::log_error_code(
831 debug_level,
832 &serde_json::to_value(&response).unwrap_or_default(),
833 );
834
835 crate::api::display_wrapper_error_guidance(&response);
837
838 let output = if raw {
840 serde_json::to_string_pretty(&response).unwrap()
841 } else {
842 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
843 let wrapped = wrap_with_envelope_and_token_type(
844 response_value,
845 "conversations.history",
846 "conv history",
847 Some(profile_name),
848 token_type,
849 )
850 .await?;
851 serde_json::to_string_pretty(&wrapped).unwrap()
852 };
853
854 println!("{}", output);
855 Ok(())
856}
857
858pub async fn run_thread_get(args: &[String]) -> Result<(), String> {
859 if has_flag(args, "--help") || has_flag(args, "-h") {
861 print_thread_usage(&args[0]);
862 return Ok(());
863 }
864
865 if args.len() < 5 {
867 return Err("Usage: slack-rs thread get <channel> <thread_ts> [--limit=N] [--inclusive] [--raw] [--profile=NAME] [--token-type=bot|user]".to_string());
868 }
869
870 let channel = args[3].clone();
871 let thread_ts = args[4].clone();
872 let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
873 let inclusive = has_flag(args, "--inclusive");
874 let profile_name = resolve_profile_name(args);
875 let token_type = parse_token_type(args)?;
876 let raw = should_output_raw(args);
877
878 let debug_level = debug::get_debug_level(args);
880
881 let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
883 "environment"
884 } else {
885 "file"
886 };
887
888 let resolved_token_type = if let Some(explicit) = token_type {
890 explicit
891 } else {
892 let config_path = default_config_path().map_err(|e| e.to_string())?;
893 let profile = resolve_profile_full(&config_path, &profile_name)
894 .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
895
896 if let Some(default_type) = profile.default_token_type {
897 default_type
898 } else {
899 let token_store = create_token_store().map_err(|e| e.to_string())?;
900 let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
901 if token_store.get(&user_token_key).is_ok() {
902 TokenType::User
903 } else {
904 TokenType::Bot
905 }
906 }
907 };
908
909 let endpoint = "https://slack.com/api/conversations.replies";
910
911 debug::log_api_context(
912 debug_level,
913 Some(&profile_name),
914 token_store_backend,
915 resolved_token_type.as_str(),
916 "conversations.replies",
917 endpoint,
918 );
919
920 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
921 let inclusive_opt = if inclusive { Some(true) } else { None };
922 let response = commands::thread_get(&client, channel, thread_ts, limit, inclusive_opt)
923 .await
924 .map_err(|e| e.to_string())?;
925
926 debug::log_error_code(
928 debug_level,
929 &serde_json::to_value(&response).unwrap_or_default(),
930 );
931
932 crate::api::display_wrapper_error_guidance(&response);
934
935 let output = if raw {
937 serde_json::to_string_pretty(&response).unwrap()
938 } else {
939 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
940 let wrapped = wrap_with_envelope_and_token_type(
941 response_value,
942 "conversations.replies",
943 "thread get",
944 Some(profile_name),
945 token_type,
946 )
947 .await?;
948 serde_json::to_string_pretty(&wrapped).unwrap()
949 };
950
951 println!("{}", output);
952 Ok(())
953}
954
955pub async fn run_users_info(args: &[String]) -> Result<(), String> {
956 let user = args[3].clone();
957 let profile_name = resolve_profile_name(args);
958 let token_type = parse_token_type(args)?;
959 let raw = should_output_raw(args);
960
961 let debug_level = debug::get_debug_level(args);
963
964 let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
966 "environment"
967 } else {
968 "file"
969 };
970
971 let resolved_token_type = if let Some(explicit) = token_type {
973 explicit
974 } else {
975 let config_path = default_config_path().map_err(|e| e.to_string())?;
976 let profile = resolve_profile_full(&config_path, &profile_name)
977 .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
978
979 if let Some(default_type) = profile.default_token_type {
980 default_type
981 } else {
982 let token_store = create_token_store().map_err(|e| e.to_string())?;
983 let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
984 if token_store.get(&user_token_key).is_ok() {
985 TokenType::User
986 } else {
987 TokenType::Bot
988 }
989 }
990 };
991
992 let endpoint = "https://slack.com/api/users.info";
993
994 debug::log_api_context(
995 debug_level,
996 Some(&profile_name),
997 token_store_backend,
998 resolved_token_type.as_str(),
999 "users.info",
1000 endpoint,
1001 );
1002
1003 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1004 let response = commands::users_info(&client, user)
1005 .await
1006 .map_err(|e| e.to_string())?;
1007
1008 debug::log_error_code(
1010 debug_level,
1011 &serde_json::to_value(&response).unwrap_or_default(),
1012 );
1013
1014 crate::api::display_wrapper_error_guidance(&response);
1016
1017 let output = if raw {
1019 serde_json::to_string_pretty(&response).unwrap()
1020 } else {
1021 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1022 let wrapped = wrap_with_envelope_and_token_type(
1023 response_value,
1024 "users.info",
1025 "users info",
1026 Some(profile_name),
1027 token_type,
1028 )
1029 .await?;
1030 serde_json::to_string_pretty(&wrapped).unwrap()
1031 };
1032
1033 println!("{}", output);
1034 Ok(())
1035}
1036
1037pub async fn run_users_cache_update(args: &[String]) -> Result<(), String> {
1038 let profile_name = resolve_profile_name(args);
1039 let force = has_flag(args, "--force");
1040 let token_type = parse_token_type(args)?;
1041
1042 let config_path = default_config_path().map_err(|e| e.to_string())?;
1043 let config = load_config(&config_path).map_err(|e| e.to_string())?;
1044
1045 let profile = config
1046 .get(&profile_name)
1047 .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
1048
1049 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1050
1051 commands::update_cache(&client, profile.team_id.clone(), force)
1052 .await
1053 .map_err(|e| e.to_string())?;
1054
1055 println!("Cache updated successfully for team {}", profile.team_id);
1056 Ok(())
1057}
1058
1059pub async fn run_users_resolve_mentions(args: &[String]) -> Result<(), String> {
1060 if args.len() < 4 {
1061 return Err(
1062 "Usage: users resolve-mentions <text> [--profile=NAME] [--format=FORMAT]".to_string(),
1063 );
1064 }
1065
1066 let text = args[3].clone();
1067 let profile_name = resolve_profile_name(args);
1068 let format_str = get_option(args, "--format=").unwrap_or_else(|| "display_name".to_string());
1069
1070 let format = format_str.parse::<commands::MentionFormat>().map_err(|_| {
1071 format!(
1072 "Invalid format: {}. Use display_name, real_name, or username",
1073 format_str
1074 )
1075 })?;
1076
1077 let config_path = default_config_path().map_err(|e| e.to_string())?;
1078 let config = load_config(&config_path).map_err(|e| e.to_string())?;
1079
1080 let profile = config
1081 .get(&profile_name)
1082 .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
1083
1084 let cache_path = commands::UsersCacheFile::default_path()?;
1085 let cache_file = commands::UsersCacheFile::load(&cache_path)?;
1086
1087 let workspace_cache = cache_file.get_workspace(&profile.team_id).ok_or_else(|| {
1088 format!(
1089 "No cache found for team {}. Run 'users cache-update' first.",
1090 profile.team_id
1091 )
1092 })?;
1093
1094 let result = commands::resolve_mentions(&text, workspace_cache, format);
1095 println!("{}", result);
1096 Ok(())
1097}
1098
1099async fn get_team_and_user_ids_from_profile(
1101 profile_name: &str,
1102) -> Result<(String, String), String> {
1103 let config_path = default_config_path().map_err(|e| e.to_string())?;
1104 let profile = resolve_profile_full(&config_path, profile_name)
1105 .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
1106 Ok((profile.team_id, profile.user_id))
1107}
1108
1109pub async fn run_msg_post(args: &[String], non_interactive: bool) -> Result<(), String> {
1110 use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1111
1112 if args.len() < 5 {
1113 return Err("Usage: msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]".to_string());
1114 }
1115
1116 let channel = args[3].clone();
1117 let text = args[4].clone();
1118 let thread_ts = get_option(args, "--thread-ts=");
1119 let reply_broadcast = has_flag(args, "--reply-broadcast");
1120 let yes = has_flag(args, "--yes");
1121 let profile_name = resolve_profile_name(args);
1122 let token_type = parse_token_type(args)?;
1123 let idempotency_key = get_option(args, "--idempotency-key=");
1124
1125 if reply_broadcast && thread_ts.is_none() {
1127 return Err("Error: --reply-broadcast requires --thread-ts".to_string());
1128 }
1129
1130 let raw = should_output_raw(args);
1131 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1132
1133 let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1135 let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1136
1137 let mut params = serde_json::Map::new();
1139 params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1140 params.insert("text".to_string(), serde_json::json!(text.clone()));
1141 if let Some(ref ts) = thread_ts {
1142 params.insert("thread_ts".to_string(), serde_json::json!(ts));
1143 if reply_broadcast {
1144 params.insert("reply_broadcast".to_string(), serde_json::json!(true));
1145 }
1146 }
1147
1148 let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1150
1151 match handler
1152 .check(
1153 Some(key.clone()),
1154 team_id.clone(),
1155 user_id.clone(),
1156 "chat.postMessage".to_string(),
1157 ¶ms,
1158 )
1159 .map_err(|e| e.to_string())?
1160 {
1161 IdempotencyCheckResult::Replay {
1162 response, status, ..
1163 } => {
1164 (response, Some(status))
1166 }
1167 IdempotencyCheckResult::Execute {
1168 key: scoped_key,
1169 fingerprint,
1170 } => {
1171 let response = commands::msg_post(
1173 &client,
1174 channel,
1175 text,
1176 thread_ts,
1177 reply_broadcast,
1178 yes,
1179 non_interactive,
1180 )
1181 .await
1182 .map_err(|e| e.to_string())?;
1183
1184 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1185
1186 handler
1188 .store(scoped_key, fingerprint, response_value.clone())
1189 .map_err(|e| e.to_string())?;
1190
1191 (
1192 response_value,
1193 Some(crate::idempotency::IdempotencyStatus::Executed),
1194 )
1195 }
1196 IdempotencyCheckResult::NoKey => unreachable!(),
1197 }
1198 } else {
1199 let response = commands::msg_post(
1201 &client,
1202 channel,
1203 text,
1204 thread_ts,
1205 reply_broadcast,
1206 yes,
1207 non_interactive,
1208 )
1209 .await
1210 .map_err(|e| e.to_string())?;
1211
1212 (
1213 serde_json::to_value(&response).map_err(|e| e.to_string())?,
1214 None,
1215 )
1216 };
1217
1218 if let Ok(api_response) =
1220 serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1221 {
1222 crate::api::display_wrapper_error_guidance(&api_response);
1223 }
1224
1225 let output = if raw {
1227 serde_json::to_string_pretty(&response_value).unwrap()
1228 } else {
1229 let mut wrapped = wrap_with_envelope_and_token_type(
1230 response_value,
1231 "chat.postMessage",
1232 "msg post",
1233 Some(profile_name),
1234 token_type,
1235 )
1236 .await?;
1237
1238 if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1240 wrapped = wrapped.with_idempotency(
1241 key,
1242 match status {
1243 crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1244 crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1245 },
1246 );
1247 }
1248
1249 serde_json::to_string_pretty(&wrapped).unwrap()
1250 };
1251
1252 println!("{}", output);
1253 Ok(())
1254}
1255
1256pub async fn run_msg_update(args: &[String], non_interactive: bool) -> Result<(), String> {
1257 use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1258
1259 if args.len() < 6 {
1260 return Err("Usage: msg update <channel> <ts> <text> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]".to_string());
1261 }
1262
1263 let channel = args[3].clone();
1264 let ts = args[4].clone();
1265 let text = args[5].clone();
1266 let yes = has_flag(args, "--yes");
1267 let profile_name = resolve_profile_name(args);
1268 let token_type = parse_token_type(args)?;
1269 let idempotency_key = get_option(args, "--idempotency-key=");
1270 let raw = should_output_raw(args);
1271
1272 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1273
1274 let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1276 let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1277
1278 let mut params = serde_json::Map::new();
1279 params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1280 params.insert("ts".to_string(), serde_json::json!(ts.clone()));
1281 params.insert("text".to_string(), serde_json::json!(text.clone()));
1282
1283 let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1284
1285 match handler
1286 .check(
1287 Some(key.clone()),
1288 team_id,
1289 user_id,
1290 "chat.update".to_string(),
1291 ¶ms,
1292 )
1293 .map_err(|e| e.to_string())?
1294 {
1295 IdempotencyCheckResult::Replay {
1296 response, status, ..
1297 } => (response, Some(status)),
1298 IdempotencyCheckResult::Execute {
1299 key: scoped_key,
1300 fingerprint,
1301 } => {
1302 let response =
1303 commands::msg_update(&client, channel, ts, text, yes, non_interactive)
1304 .await
1305 .map_err(|e| e.to_string())?;
1306 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1307 handler
1308 .store(scoped_key, fingerprint, response_value.clone())
1309 .map_err(|e| e.to_string())?;
1310 (
1311 response_value,
1312 Some(crate::idempotency::IdempotencyStatus::Executed),
1313 )
1314 }
1315 IdempotencyCheckResult::NoKey => unreachable!(),
1316 }
1317 } else {
1318 let response = commands::msg_update(&client, channel, ts, text, yes, non_interactive)
1319 .await
1320 .map_err(|e| e.to_string())?;
1321 (
1322 serde_json::to_value(&response).map_err(|e| e.to_string())?,
1323 None,
1324 )
1325 };
1326
1327 if let Ok(api_response) =
1328 serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1329 {
1330 crate::api::display_wrapper_error_guidance(&api_response);
1331 }
1332
1333 let output = if raw {
1334 serde_json::to_string_pretty(&response_value).unwrap()
1335 } else {
1336 let mut wrapped = wrap_with_envelope_and_token_type(
1337 response_value,
1338 "chat.update",
1339 "msg update",
1340 Some(profile_name),
1341 token_type,
1342 )
1343 .await?;
1344
1345 if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1346 wrapped = wrapped.with_idempotency(
1347 key,
1348 match status {
1349 crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1350 crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1351 },
1352 );
1353 }
1354
1355 serde_json::to_string_pretty(&wrapped).unwrap()
1356 };
1357
1358 println!("{}", output);
1359 Ok(())
1360}
1361
1362pub async fn run_msg_delete(args: &[String], non_interactive: bool) -> Result<(), String> {
1363 use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1364
1365 if args.len() < 5 {
1366 return Err(
1367 "Usage: msg delete <channel> <ts> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]"
1368 .to_string(),
1369 );
1370 }
1371
1372 let channel = args[3].clone();
1373 let ts = args[4].clone();
1374 let yes = has_flag(args, "--yes");
1375 let profile_name = resolve_profile_name(args);
1376 let token_type = parse_token_type(args)?;
1377 let idempotency_key = get_option(args, "--idempotency-key=");
1378 let raw = should_output_raw(args);
1379
1380 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1381
1382 let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1383 let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1384 let mut params = serde_json::Map::new();
1385 params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1386 params.insert("ts".to_string(), serde_json::json!(ts.clone()));
1387 let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1388 match handler
1389 .check(
1390 Some(key.clone()),
1391 team_id,
1392 user_id,
1393 "chat.delete".to_string(),
1394 ¶ms,
1395 )
1396 .map_err(|e| e.to_string())?
1397 {
1398 IdempotencyCheckResult::Replay {
1399 response, status, ..
1400 } => (response, Some(status)),
1401 IdempotencyCheckResult::Execute {
1402 key: scoped_key,
1403 fingerprint,
1404 } => {
1405 let response = commands::msg_delete(&client, channel, ts, yes, non_interactive)
1406 .await
1407 .map_err(|e| e.to_string())?;
1408 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1409 handler
1410 .store(scoped_key, fingerprint, response_value.clone())
1411 .map_err(|e| e.to_string())?;
1412 (
1413 response_value,
1414 Some(crate::idempotency::IdempotencyStatus::Executed),
1415 )
1416 }
1417 IdempotencyCheckResult::NoKey => unreachable!(),
1418 }
1419 } else {
1420 let response = commands::msg_delete(&client, channel, ts, yes, non_interactive)
1421 .await
1422 .map_err(|e| e.to_string())?;
1423 (
1424 serde_json::to_value(&response).map_err(|e| e.to_string())?,
1425 None,
1426 )
1427 };
1428
1429 if let Ok(api_response) =
1430 serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1431 {
1432 crate::api::display_wrapper_error_guidance(&api_response);
1433 }
1434
1435 let output = if raw {
1436 serde_json::to_string_pretty(&response_value).unwrap()
1437 } else {
1438 let mut wrapped = wrap_with_envelope_and_token_type(
1439 response_value,
1440 "chat.delete",
1441 "msg delete",
1442 Some(profile_name),
1443 token_type,
1444 )
1445 .await?;
1446 if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1447 wrapped = wrapped.with_idempotency(
1448 key,
1449 match status {
1450 crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1451 crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1452 },
1453 );
1454 }
1455 serde_json::to_string_pretty(&wrapped).unwrap()
1456 };
1457
1458 println!("{}", output);
1459 Ok(())
1460}
1461
1462pub async fn run_react_add(args: &[String], non_interactive: bool) -> Result<(), String> {
1463 use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1464
1465 if args.len() < 6 {
1466 return Err(
1467 "Usage: react add <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]"
1468 .to_string(),
1469 );
1470 }
1471
1472 let channel = args[3].clone();
1473 let ts = args[4].clone();
1474 let emoji = args[5].clone();
1475 let yes = has_flag(args, "--yes");
1476 let profile_name = resolve_profile_name(args);
1477 let token_type = parse_token_type(args)?;
1478 let idempotency_key = get_option(args, "--idempotency-key=");
1479 let raw = should_output_raw(args);
1480
1481 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1482
1483 let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1484 let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1485 let mut params = serde_json::Map::new();
1486 params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1487 params.insert("timestamp".to_string(), serde_json::json!(ts.clone()));
1488 params.insert("name".to_string(), serde_json::json!(emoji.clone()));
1489 let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1490 match handler
1491 .check(
1492 Some(key.clone()),
1493 team_id,
1494 user_id,
1495 "reactions.add".to_string(),
1496 ¶ms,
1497 )
1498 .map_err(|e| e.to_string())?
1499 {
1500 IdempotencyCheckResult::Replay {
1501 response, status, ..
1502 } => (response, Some(status)),
1503 IdempotencyCheckResult::Execute {
1504 key: scoped_key,
1505 fingerprint,
1506 } => {
1507 let response =
1508 commands::react_add(&client, channel, ts, emoji, yes, non_interactive)
1509 .await
1510 .map_err(|e| e.to_string())?;
1511 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1512 handler
1513 .store(scoped_key, fingerprint, response_value.clone())
1514 .map_err(|e| e.to_string())?;
1515 (
1516 response_value,
1517 Some(crate::idempotency::IdempotencyStatus::Executed),
1518 )
1519 }
1520 IdempotencyCheckResult::NoKey => unreachable!(),
1521 }
1522 } else {
1523 let response = commands::react_add(&client, channel, ts, emoji, yes, non_interactive)
1524 .await
1525 .map_err(|e| e.to_string())?;
1526 (
1527 serde_json::to_value(&response).map_err(|e| e.to_string())?,
1528 None,
1529 )
1530 };
1531
1532 if let Ok(api_response) =
1533 serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1534 {
1535 crate::api::display_wrapper_error_guidance(&api_response);
1536 }
1537
1538 let output = if raw {
1539 serde_json::to_string_pretty(&response_value).unwrap()
1540 } else {
1541 let mut wrapped = wrap_with_envelope_and_token_type(
1542 response_value,
1543 "reactions.add",
1544 "react add",
1545 Some(profile_name),
1546 token_type,
1547 )
1548 .await?;
1549 if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1550 wrapped = wrapped.with_idempotency(
1551 key,
1552 match status {
1553 crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1554 crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1555 },
1556 );
1557 }
1558 serde_json::to_string_pretty(&wrapped).unwrap()
1559 };
1560
1561 println!("{}", output);
1562 Ok(())
1563}
1564
1565pub async fn run_react_remove(args: &[String], non_interactive: bool) -> Result<(), String> {
1566 use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1567
1568 if args.len() < 6 {
1569 return Err(
1570 "Usage: react remove <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]".to_string(),
1571 );
1572 }
1573
1574 let channel = args[3].clone();
1575 let ts = args[4].clone();
1576 let emoji = args[5].clone();
1577 let yes = has_flag(args, "--yes");
1578 let profile_name = resolve_profile_name(args);
1579 let token_type = parse_token_type(args)?;
1580 let idempotency_key = get_option(args, "--idempotency-key=");
1581 let raw = should_output_raw(args);
1582
1583 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1584
1585 let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1586 let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1587 let mut params = serde_json::Map::new();
1588 params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1589 params.insert("timestamp".to_string(), serde_json::json!(ts.clone()));
1590 params.insert("name".to_string(), serde_json::json!(emoji.clone()));
1591 let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1592 match handler
1593 .check(
1594 Some(key.clone()),
1595 team_id,
1596 user_id,
1597 "reactions.remove".to_string(),
1598 ¶ms,
1599 )
1600 .map_err(|e| e.to_string())?
1601 {
1602 IdempotencyCheckResult::Replay {
1603 response, status, ..
1604 } => (response, Some(status)),
1605 IdempotencyCheckResult::Execute {
1606 key: scoped_key,
1607 fingerprint,
1608 } => {
1609 let response =
1610 commands::react_remove(&client, channel, ts, emoji, yes, non_interactive)
1611 .await
1612 .map_err(|e| e.to_string())?;
1613 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1614 handler
1615 .store(scoped_key, fingerprint, response_value.clone())
1616 .map_err(|e| e.to_string())?;
1617 (
1618 response_value,
1619 Some(crate::idempotency::IdempotencyStatus::Executed),
1620 )
1621 }
1622 IdempotencyCheckResult::NoKey => unreachable!(),
1623 }
1624 } else {
1625 let response = commands::react_remove(&client, channel, ts, emoji, yes, non_interactive)
1626 .await
1627 .map_err(|e| e.to_string())?;
1628 (
1629 serde_json::to_value(&response).map_err(|e| e.to_string())?,
1630 None,
1631 )
1632 };
1633
1634 if let Ok(api_response) =
1635 serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1636 {
1637 crate::api::display_wrapper_error_guidance(&api_response);
1638 }
1639
1640 let output = if raw {
1641 serde_json::to_string_pretty(&response_value).unwrap()
1642 } else {
1643 let mut wrapped = wrap_with_envelope_and_token_type(
1644 response_value,
1645 "reactions.remove",
1646 "react remove",
1647 Some(profile_name),
1648 token_type,
1649 )
1650 .await?;
1651 if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1652 wrapped = wrapped.with_idempotency(
1653 key,
1654 match status {
1655 crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1656 crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1657 },
1658 );
1659 }
1660 serde_json::to_string_pretty(&wrapped).unwrap()
1661 };
1662
1663 println!("{}", output);
1664 Ok(())
1665}
1666
1667pub async fn run_file_upload(args: &[String], non_interactive: bool) -> Result<(), String> {
1668 use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1669
1670 if args.len() < 4 {
1671 return Err(
1672 "Usage: file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]"
1673 .to_string(),
1674 );
1675 }
1676
1677 let file_path = args[3].clone();
1678 let channels = get_option(args, "--channel=").or_else(|| get_option(args, "--channels="));
1679 let title = get_option(args, "--title=");
1680 let comment = get_option(args, "--comment=");
1681 let yes = has_flag(args, "--yes");
1682 let profile_name = resolve_profile_name(args);
1683 let token_type = parse_token_type(args)?;
1684 let idempotency_key = get_option(args, "--idempotency-key=");
1685 let raw = should_output_raw(args);
1686
1687 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1688
1689 let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1690 let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1691 let mut params = serde_json::Map::new();
1692 params.insert("filename".to_string(), serde_json::json!(file_path.clone()));
1693 if let Some(ref ch) = channels {
1694 params.insert("channels".to_string(), serde_json::json!(ch));
1695 }
1696 if let Some(ref t) = title {
1697 params.insert("title".to_string(), serde_json::json!(t));
1698 }
1699 if let Some(ref c) = comment {
1700 params.insert("comment".to_string(), serde_json::json!(c));
1701 }
1702 let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1703 match handler
1704 .check(
1705 Some(key.clone()),
1706 team_id,
1707 user_id,
1708 "files.upload".to_string(),
1709 ¶ms,
1710 )
1711 .map_err(|e| e.to_string())?
1712 {
1713 IdempotencyCheckResult::Replay {
1714 response, status, ..
1715 } => (response, Some(status)),
1716 IdempotencyCheckResult::Execute {
1717 key: scoped_key,
1718 fingerprint,
1719 } => {
1720 let response = commands::file_upload(
1721 &client,
1722 file_path,
1723 channels,
1724 title,
1725 comment,
1726 yes,
1727 non_interactive,
1728 )
1729 .await
1730 .map_err(|e| e.to_string())?;
1731 let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1732 handler
1733 .store(scoped_key, fingerprint, response_value.clone())
1734 .map_err(|e| e.to_string())?;
1735 (
1736 response_value,
1737 Some(crate::idempotency::IdempotencyStatus::Executed),
1738 )
1739 }
1740 IdempotencyCheckResult::NoKey => unreachable!(),
1741 }
1742 } else {
1743 let response = commands::file_upload(
1744 &client,
1745 file_path,
1746 channels,
1747 title,
1748 comment,
1749 yes,
1750 non_interactive,
1751 )
1752 .await
1753 .map_err(|e| e.to_string())?;
1754 (
1755 serde_json::to_value(&response).map_err(|e| e.to_string())?,
1756 None,
1757 )
1758 };
1759
1760 crate::api::display_json_error_guidance(&response_value);
1761
1762 let output = if raw {
1763 serde_json::to_string_pretty(&response_value).unwrap()
1764 } else {
1765 let mut wrapped = wrap_with_envelope_and_token_type(
1766 response_value,
1767 "files.upload",
1768 "file upload",
1769 Some(profile_name),
1770 token_type,
1771 )
1772 .await?;
1773 if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1774 wrapped = wrapped.with_idempotency(
1775 key,
1776 match status {
1777 crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1778 crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1779 },
1780 );
1781 }
1782 serde_json::to_string_pretty(&wrapped).unwrap()
1783 };
1784
1785 println!("{}", output);
1786 Ok(())
1787}
1788
1789pub async fn run_file_download(args: &[String]) -> Result<(), String> {
1790 if args.len() < 3 {
1791 return Err(
1792 "Usage: file download [<file_id>] [--url=URL] [--out=PATH] [--profile=NAME] [--token-type=bot|user]"
1793 .to_string(),
1794 );
1795 }
1796
1797 let file_id = args.get(3).filter(|arg| !arg.starts_with("--")).cloned();
1799 let url = get_option(args, "--url=");
1800 let out = get_option(args, "--out=");
1801 let profile_name = resolve_profile_name(args);
1802 let token_type = parse_token_type(args)?;
1803 let raw = should_output_raw(args);
1804
1805 if file_id.is_none() && url.is_none() {
1807 return Err("Either <file_id> or --url must be provided".to_string());
1808 }
1809
1810 let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1811 let response = commands::file_download(&client, file_id, url, out)
1812 .await
1813 .map_err(|e| e.to_string())?;
1814
1815 if let Some(out_path) = response.get("output").and_then(|v| v.as_str()) {
1817 if out_path == "-" {
1818 return Ok(());
1819 }
1820 }
1821
1822 crate::api::display_json_error_guidance(&response);
1824
1825 let output = if raw {
1827 serde_json::to_string_pretty(&response).unwrap()
1828 } else {
1829 let wrapped = wrap_with_envelope_and_token_type(
1830 response,
1831 "files.info + download",
1832 "file download",
1833 Some(profile_name),
1834 token_type,
1835 )
1836 .await?;
1837 serde_json::to_string_pretty(&wrapped).unwrap()
1838 };
1839
1840 println!("{}", output);
1841 Ok(())
1842}
1843
1844pub fn print_conv_usage(prog: &str) {
1845 println!("Conv command usage:");
1846 println!(
1847 " {} conv list [--types=TYPE] [--include-private] [--all] [--limit=N] [--filter=KEY:VALUE]... [--format=FORMAT] [--sort=KEY] [--sort-dir=DIR] [--raw] [--profile=NAME] [--token-type=bot|user]",
1848 prog
1849 );
1850 println!(" List conversations with optional filtering and sorting");
1851 println!(" Options accept both --option=value and --option value formats");
1852 println!(" Default: Includes public and private channels (limit=1000, auto-paginated)");
1853 println!(" Type shortcuts (mutually exclusive with --types):");
1854 println!(" - --include-private: Include private channels (same as default now)");
1855 println!(
1856 " - --all: Include all conversation types (public_channel,private_channel,im,mpim)"
1857 );
1858 println!(" Filters: name:<glob>, is_member:true|false, is_private:true|false");
1859 println!(" - name:<glob>: Filter by channel name (supports * and ? wildcards)");
1860 println!(" - is_member:true|false: Filter by membership status");
1861 println!(" - is_private:true|false: Filter by channel privacy");
1862 println!(" Formats: json (default), jsonl, table, tsv");
1863 println!(" - json: JSON format with envelope (use --raw for raw Slack API response)");
1864 println!(" - jsonl: JSON Lines format (one object per line)");
1865 println!(" - table: Human-readable table format");
1866 println!(" - tsv: Tab-separated values");
1867 println!(" Sort keys: name, created, num_members");
1868 println!(" - name: Sort by channel name");
1869 println!(" - created: Sort by creation timestamp");
1870 println!(" - num_members: Sort by member count");
1871 println!(" Sort direction: asc (default), desc");
1872 println!(" Note: --raw is only valid with --format json");
1873 println!();
1874 println!(
1875 " {} conv search <pattern> [--select] [--types=TYPE] [--limit=N] [--filter=KEY:VALUE]... [--format=FORMAT] [--sort=KEY] [--sort-dir=DIR] [--raw] [--profile=NAME] [--token-type=bot|user]",
1876 prog
1877 );
1878 println!(" Search conversations by name pattern (applies name:<pattern> filter)");
1879 println!(" Default: Includes public and private channels (limit=1000, auto-paginated)");
1880 println!(" Options accept both --option=value and --option value formats");
1881 println!(" --select: Interactively select from results and output channel ID only");
1882 println!();
1883 println!(
1884 " {} conv select [--types=TYPE] [--filter=KEY:VALUE]... [--profile=NAME]",
1885 prog
1886 );
1887 println!(" Interactively select a conversation and output its channel ID");
1888 println!(" Default: Includes public and private channels (limit=1000, auto-paginated)");
1889 println!(" Options accept both --option=value and --option value formats");
1890 println!();
1891 println!(
1892 " {} conv history <channel> [--limit=N] [--oldest=TS] [--latest=TS] [--profile=NAME] [--token-type=bot|user]",
1893 prog
1894 );
1895 println!(
1896 " {} conv history --interactive [--types=TYPE] [--filter=KEY:VALUE]... [--limit=N] [--profile=NAME]",
1897 prog
1898 );
1899 println!(" Select channel interactively before fetching history");
1900 println!(" Default: Includes public and private channels (limit=1000, auto-paginated)");
1901 println!(" Options accept both --option=value and --option value formats");
1902}
1903
1904pub fn print_thread_usage(prog: &str) {
1905 println!("Thread command usage:");
1906 println!(
1907 " {} thread get <channel> <thread_ts> [--limit=N] [--inclusive] [--raw] [--profile=NAME] [--token-type=bot|user]",
1908 prog
1909 );
1910 println!(" Get thread messages (conversation replies) for a specific thread");
1911 println!(" Arguments:");
1912 println!(" <channel> - Channel ID containing the thread");
1913 println!(" <thread_ts> - Timestamp of the parent message (thread identifier)");
1914 println!(" Options:");
1915 println!(" --limit=N - Number of messages per page (default: 100)");
1916 println!(" --inclusive - Include the parent message in results");
1917 println!(" --raw - Output raw Slack API response without envelope");
1918 println!(" --profile=NAME - Profile to use (default: 'default')");
1919 println!(" --token-type=TYPE - Token type to use (bot or user)");
1920 println!(" Note: Automatically follows pagination to retrieve all thread messages");
1921}
1922
1923pub fn print_users_usage(prog: &str) {
1924 println!("Users command usage:");
1925 println!(
1926 " {} users info <user_id> [--profile=NAME] [--token-type=bot|user]",
1927 prog
1928 );
1929 println!(
1930 " {} users cache-update [--profile=NAME] [--force] [--token-type=bot|user]",
1931 prog
1932 );
1933 println!(" {} users resolve-mentions <text> [--profile=NAME] [--format=display_name|real_name|username]", prog);
1934 println!(" Options accept both --option=value and --option value formats");
1935}
1936
1937pub fn print_msg_usage(prog: &str) {
1938 println!("Msg command usage:");
1939 println!(
1940 " {} msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1941 prog
1942 );
1943 println!(" Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1944 println!(
1945 " {} msg update <channel> <ts> <text> [--yes] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1946 prog
1947 );
1948 println!(" Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1949 println!(
1950 " {} msg delete <channel> <ts> [--yes] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1951 prog
1952 );
1953 println!(" Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1954 println!(" Options accept both --option=value and --option value formats");
1955 println!(" --idempotency-key: Prevent duplicate writes (replays stored result on retry)");
1956}
1957
1958pub fn print_react_usage(prog: &str) {
1959 println!("React command usage:");
1960 println!(
1961 " {} react add <channel> <ts> <emoji> [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1962 prog
1963 );
1964 println!(" Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1965 println!(
1966 " {} react remove <channel> <ts> <emoji> [--yes] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1967 prog
1968 );
1969 println!(" Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1970 println!(" Options accept both --option=value and --option value formats");
1971 println!(" --idempotency-key: Prevent duplicate writes (replays stored result on retry)");
1972}
1973
1974pub fn print_file_usage(prog: &str) {
1975 println!("File command usage:");
1976 println!(
1977 " {} file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1978 prog
1979 );
1980 println!(" Upload a file using external upload method");
1981 println!(" Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1982 println!(
1983 " {} file download [<file_id>] [--url=URL] [--out=PATH] [--profile=NAME] [--token-type=bot|user]",
1984 prog
1985 );
1986 println!(" Download a file from Slack");
1987 println!(" Either <file_id> or --url must be provided");
1988 println!(" --out: Output path (omit for current directory, '-' for stdout, directory for auto-naming)");
1989 println!(" Options accept both --option=value and --option value formats");
1990 println!(" --idempotency-key: Prevent duplicate writes (replays stored result on retry, upload only)");
1991}
1992
1993#[cfg(test)]
1994mod tests {
1995 use super::*;
1996
1997 #[test]
1998 fn test_parse_token_type_equals_format() {
1999 let args = vec!["command".to_string(), "--token-type=user".to_string()];
2000 let result = parse_token_type(&args).unwrap();
2001 assert_eq!(result, Some(TokenType::User));
2002 }
2003
2004 #[test]
2005 fn test_parse_token_type_space_separated() {
2006 let args = vec![
2007 "command".to_string(),
2008 "--token-type".to_string(),
2009 "bot".to_string(),
2010 ];
2011 let result = parse_token_type(&args).unwrap();
2012 assert_eq!(result, Some(TokenType::Bot));
2013 }
2014
2015 #[test]
2016 fn test_parse_token_type_both_values() {
2017 let args1 = vec!["--token-type=user".to_string()];
2019 assert_eq!(parse_token_type(&args1).unwrap(), Some(TokenType::User));
2020
2021 let args2 = vec!["--token-type=bot".to_string()];
2023 assert_eq!(parse_token_type(&args2).unwrap(), Some(TokenType::Bot));
2024
2025 let args3 = vec!["--token-type".to_string(), "user".to_string()];
2027 assert_eq!(parse_token_type(&args3).unwrap(), Some(TokenType::User));
2028
2029 let args4 = vec!["--token-type".to_string(), "bot".to_string()];
2031 assert_eq!(parse_token_type(&args4).unwrap(), Some(TokenType::Bot));
2032 }
2033
2034 #[test]
2035 fn test_parse_token_type_missing() {
2036 let args = vec!["command".to_string()];
2037 let result = parse_token_type(&args).unwrap();
2038 assert_eq!(result, None);
2039 }
2040
2041 #[test]
2042 fn test_parse_token_type_missing_value() {
2043 let args = vec!["--token-type".to_string()];
2044 let result = parse_token_type(&args);
2045 assert!(result.is_err());
2046 assert_eq!(
2047 result.unwrap_err(),
2048 "--token-type requires a value (bot or user)"
2049 );
2050 }
2051
2052 #[test]
2053 fn test_parse_token_type_invalid_value() {
2054 let args = vec!["--token-type=invalid".to_string()];
2055 let result = parse_token_type(&args);
2056 assert!(result.is_err());
2057 }
2058
2059 struct MockTokenStore {
2061 tokens: std::collections::HashMap<String, String>,
2062 }
2063
2064 impl MockTokenStore {
2065 fn new() -> Self {
2066 Self {
2067 tokens: std::collections::HashMap::new(),
2068 }
2069 }
2070
2071 fn with_token(mut self, key: &str, value: &str) -> Self {
2072 self.tokens.insert(key.to_string(), value.to_string());
2073 self
2074 }
2075 }
2076
2077 impl TokenStore for MockTokenStore {
2078 fn get(&self, key: &str) -> crate::profile::token_store::Result<String> {
2079 use crate::profile::token_store::TokenStoreError;
2080 self.tokens
2081 .get(key)
2082 .cloned()
2083 .ok_or_else(|| TokenStoreError::NotFound(key.to_string()))
2084 }
2085
2086 fn set(&self, _key: &str, _value: &str) -> crate::profile::token_store::Result<()> {
2087 unimplemented!("set not needed for tests")
2088 }
2089
2090 fn delete(&self, _key: &str) -> crate::profile::token_store::Result<()> {
2091 unimplemented!("delete not needed for tests")
2092 }
2093
2094 fn exists(&self, key: &str) -> bool {
2095 self.tokens.contains_key(key)
2096 }
2097 }
2098
2099 #[test]
2100 fn test_resolve_token_prefers_env() {
2101 let store = MockTokenStore::new().with_token("T123:U123", "xoxb-store-token");
2103
2104 let result = resolve_token_for_wrapper(
2105 Some("xoxb-env-token".to_string()),
2106 &store,
2107 "T123:U123",
2108 None,
2109 false,
2110 );
2111
2112 assert!(result.is_ok());
2113 assert_eq!(result.unwrap(), "xoxb-env-token");
2114 }
2115
2116 #[test]
2117 fn test_resolve_token_uses_store() {
2118 let store = MockTokenStore::new().with_token("T123:U123", "xoxb-store-token");
2120
2121 let result = resolve_token_for_wrapper(None, &store, "T123:U123", None, false);
2122
2123 assert!(result.is_ok());
2124 assert_eq!(result.unwrap(), "xoxb-store-token");
2125 }
2126
2127 #[test]
2128 fn test_resolve_token_explicit_request() {
2129 let store = MockTokenStore::new().with_token("T123:U123", "xoxb-bot-token");
2131
2132 let result = resolve_token_for_wrapper(
2133 None,
2134 &store,
2135 "T123:U123:user", Some("T123:U123"), true, );
2139
2140 assert!(result.is_err());
2141 assert!(result.unwrap_err().contains("explicitly requested"));
2142 }
2143
2144 #[test]
2145 fn test_resolve_token_fallback_when_not_explicit() {
2146 let store = MockTokenStore::new().with_token("T123:U123", "xoxb-bot-token");
2148
2149 let result = resolve_token_for_wrapper(
2150 None,
2151 &store,
2152 "T123:U123:user", Some("T123:U123"), false, );
2156
2157 assert!(result.is_ok());
2158 assert_eq!(result.unwrap(), "xoxb-bot-token");
2159 }
2160
2161 #[test]
2162 fn test_resolve_token_env_overrides_explicit() {
2163 let store = MockTokenStore::new()
2165 .with_token("T123:U123", "xoxb-bot-token")
2166 .with_token("T123:U123:user", "xoxp-user-token");
2167
2168 let result = resolve_token_for_wrapper(
2169 Some("xoxb-env-token".to_string()),
2170 &store,
2171 "T123:U123:user",
2172 None,
2173 true, );
2175
2176 assert!(result.is_ok());
2177 assert_eq!(result.unwrap(), "xoxb-env-token");
2178 }
2179
2180 #[test]
2182 fn test_get_option_equals_format() {
2183 let args = vec!["cmd".to_string(), "--filter=is_private:true".to_string()];
2184 assert_eq!(
2185 get_option(&args, "--filter="),
2186 Some("is_private:true".to_string())
2187 );
2188 }
2189
2190 #[test]
2191 fn test_get_option_space_separated() {
2192 let args = vec![
2193 "cmd".to_string(),
2194 "--filter".to_string(),
2195 "is_private:true".to_string(),
2196 ];
2197 assert_eq!(
2198 get_option(&args, "--filter="),
2199 Some("is_private:true".to_string())
2200 );
2201 }
2202
2203 #[test]
2204 fn test_get_option_space_separated_rejects_dash_value() {
2205 let args = vec![
2207 "cmd".to_string(),
2208 "--filter".to_string(),
2209 "--other".to_string(),
2210 ];
2211 assert_eq!(get_option(&args, "--filter="), None);
2212 }
2213
2214 #[test]
2215 fn test_get_option_space_separated_missing_value() {
2216 let args = vec!["cmd".to_string(), "--filter".to_string()];
2217 assert_eq!(get_option(&args, "--filter="), None);
2218 }
2219
2220 #[test]
2221 fn test_get_option_prefers_equals_format() {
2222 let args = vec![
2224 "--filter=value1".to_string(),
2225 "--filter".to_string(),
2226 "value2".to_string(),
2227 ];
2228 assert_eq!(get_option(&args, "--filter="), Some("value1".to_string()));
2229 }
2230
2231 #[test]
2233 fn test_get_all_options_equals_format() {
2234 let args = vec![
2235 "cmd".to_string(),
2236 "--filter=is_private:true".to_string(),
2237 "--filter=is_member:true".to_string(),
2238 ];
2239 let result = get_all_options(&args, "--filter=");
2240 assert_eq!(result, vec!["is_private:true", "is_member:true"]);
2241 }
2242
2243 #[test]
2244 fn test_get_all_options_space_separated() {
2245 let args = vec![
2246 "cmd".to_string(),
2247 "--filter".to_string(),
2248 "is_private:true".to_string(),
2249 "--filter".to_string(),
2250 "is_member:true".to_string(),
2251 ];
2252 let result = get_all_options(&args, "--filter=");
2253 assert_eq!(result, vec!["is_private:true", "is_member:true"]);
2254 }
2255
2256 #[test]
2257 fn test_get_all_options_mixed_format() {
2258 let args = vec![
2259 "cmd".to_string(),
2260 "--filter=is_private:true".to_string(),
2261 "--filter".to_string(),
2262 "is_member:true".to_string(),
2263 "--filter=name:test".to_string(),
2264 "--filter".to_string(),
2265 "is_archived:false".to_string(),
2266 ];
2267 let result = get_all_options(&args, "--filter=");
2268 assert_eq!(
2269 result,
2270 vec![
2271 "is_private:true",
2272 "name:test",
2273 "is_member:true",
2274 "is_archived:false"
2275 ]
2276 );
2277 }
2278
2279 #[test]
2280 fn test_get_all_options_rejects_dash_values() {
2281 let args = vec![
2282 "cmd".to_string(),
2283 "--filter=value1".to_string(),
2284 "--filter".to_string(),
2285 "--other".to_string(), "--filter".to_string(),
2287 "value2".to_string(),
2288 ];
2289 let result = get_all_options(&args, "--filter=");
2290 assert_eq!(result, vec!["value1", "value2"]);
2291 }
2292
2293 #[test]
2294 fn test_get_all_options_space_separated_at_end() {
2295 let args = vec![
2297 "cmd".to_string(),
2298 "--filter=value1".to_string(),
2299 "--filter".to_string(),
2300 ];
2301 let result = get_all_options(&args, "--filter=");
2302 assert_eq!(result, vec!["value1"]);
2303 }
2304
2305 #[test]
2307 fn test_conv_list_filter_space_separated() {
2308 let args = vec![
2310 "slack".to_string(),
2311 "conv".to_string(),
2312 "list".to_string(),
2313 "--filter".to_string(),
2314 "is_private:true".to_string(),
2315 ];
2316 let filters = get_all_options(&args, "--filter=");
2317 assert_eq!(filters.len(), 1);
2318 assert_eq!(filters[0], "is_private:true");
2319 }
2320
2321 #[test]
2322 fn test_conv_list_multiple_filters_mixed() {
2323 let args = vec![
2324 "slack".to_string(),
2325 "conv".to_string(),
2326 "list".to_string(),
2327 "--filter=is_private:true".to_string(),
2328 "--filter".to_string(),
2329 "is_member:true".to_string(),
2330 ];
2331 let filters = get_all_options(&args, "--filter=");
2332 assert_eq!(filters.len(), 2);
2333 assert_eq!(filters[0], "is_private:true");
2334 assert_eq!(filters[1], "is_member:true");
2335 }
2336
2337 #[test]
2338 fn test_conv_search_options_space_separated() {
2339 let args = vec![
2340 "slack".to_string(),
2341 "conv".to_string(),
2342 "search".to_string(),
2343 "pattern".to_string(),
2344 "--format".to_string(),
2345 "table".to_string(),
2346 "--sort".to_string(),
2347 "name".to_string(),
2348 ];
2349 assert_eq!(get_option(&args, "--format="), Some("table".to_string()));
2350 assert_eq!(get_option(&args, "--sort="), Some("name".to_string()));
2351 }
2352
2353 #[test]
2354 fn test_search_command_options_space_separated() {
2355 let args = vec![
2356 "slack".to_string(),
2357 "search".to_string(),
2358 "query".to_string(),
2359 "--count".to_string(),
2360 "10".to_string(),
2361 "--sort".to_string(),
2362 "timestamp".to_string(),
2363 ];
2364 assert_eq!(get_option(&args, "--count="), Some("10".to_string()));
2365 assert_eq!(get_option(&args, "--sort="), Some("timestamp".to_string()));
2366 }
2367
2368 #[test]
2370 fn test_resolve_profile_name_with_equals_format() {
2371 let args = vec![
2372 "slack".to_string(),
2373 "api".to_string(),
2374 "call".to_string(),
2375 "--profile=myprofile".to_string(),
2376 "test.method".to_string(),
2377 ];
2378 assert_eq!(resolve_profile_name(&args), "myprofile");
2379 }
2380
2381 #[test]
2382 fn test_resolve_profile_name_with_space_format() {
2383 let args = vec![
2384 "slack".to_string(),
2385 "api".to_string(),
2386 "call".to_string(),
2387 "--profile".to_string(),
2388 "myprofile".to_string(),
2389 "test.method".to_string(),
2390 ];
2391 assert_eq!(resolve_profile_name(&args), "myprofile");
2392 }
2393
2394 #[test]
2395 fn test_resolve_profile_name_at_beginning() {
2396 let args = vec![
2397 "slack".to_string(),
2398 "--profile=myprofile".to_string(),
2399 "api".to_string(),
2400 "call".to_string(),
2401 "test.method".to_string(),
2402 ];
2403 assert_eq!(resolve_profile_name(&args), "myprofile");
2404 }
2405
2406 #[test]
2407 fn test_resolve_profile_name_at_end() {
2408 let args = vec![
2409 "slack".to_string(),
2410 "api".to_string(),
2411 "call".to_string(),
2412 "test.method".to_string(),
2413 "--profile=myprofile".to_string(),
2414 ];
2415 assert_eq!(resolve_profile_name(&args), "myprofile");
2416 }
2417
2418 #[test]
2419 #[serial_test::serial]
2420 fn test_resolve_profile_name_env_fallback() {
2421 std::env::set_var("SLACK_PROFILE", "envprofile");
2423
2424 let args = vec!["slack".to_string(), "api".to_string(), "call".to_string()];
2425 assert_eq!(resolve_profile_name(&args), "envprofile");
2426
2427 std::env::remove_var("SLACK_PROFILE");
2429 }
2430
2431 #[test]
2432 #[serial_test::serial]
2433 fn test_resolve_profile_name_default_fallback() {
2434 std::env::remove_var("SLACK_PROFILE");
2436
2437 let args = vec!["slack".to_string(), "api".to_string(), "call".to_string()];
2438 assert_eq!(resolve_profile_name(&args), "default");
2439 }
2440
2441 #[test]
2442 #[serial_test::serial]
2443 fn test_resolve_profile_name_flag_overrides_env() {
2444 std::env::set_var("SLACK_PROFILE", "envprofile");
2446
2447 let args = vec![
2448 "slack".to_string(),
2449 "api".to_string(),
2450 "--profile=flagprofile".to_string(),
2451 "call".to_string(),
2452 ];
2453 assert_eq!(resolve_profile_name(&args), "flagprofile");
2454
2455 std::env::remove_var("SLACK_PROFILE");
2457 }
2458
2459 #[test]
2460 #[serial_test::serial]
2461 fn test_resolve_profile_name_priority_all_sources() {
2462 std::env::set_var("SLACK_PROFILE", "envprofile");
2464
2465 let args = vec![
2467 "--profile".to_string(),
2468 "flagprofile".to_string(),
2469 "slack".to_string(),
2470 "api".to_string(),
2471 "call".to_string(),
2472 ];
2473 assert_eq!(resolve_profile_name(&args), "flagprofile");
2474
2475 std::env::remove_var("SLACK_PROFILE");
2477 }
2478
2479 #[test]
2480 fn test_resolve_profile_name_mixed_formats() {
2481 let args = vec![
2483 "slack".to_string(),
2484 "--profile=profile1".to_string(),
2485 "api".to_string(),
2486 "--profile".to_string(),
2487 "profile2".to_string(),
2488 "call".to_string(),
2489 ];
2490 assert_eq!(resolve_profile_name(&args), "profile1");
2492 }
2493
2494 #[test]
2495 fn test_conv_list_include_private_flag() {
2496 let args = vec![
2497 "slack".to_string(),
2498 "conv".to_string(),
2499 "list".to_string(),
2500 "--include-private".to_string(),
2501 ];
2502 assert!(has_flag(&args, "--include-private"));
2503 assert!(!has_flag(&args, "--all"));
2504 }
2505
2506 #[test]
2507 fn test_conv_list_all_flag() {
2508 let args = vec![
2509 "slack".to_string(),
2510 "conv".to_string(),
2511 "list".to_string(),
2512 "--all".to_string(),
2513 ];
2514 assert!(!has_flag(&args, "--include-private"));
2515 assert!(has_flag(&args, "--all"));
2516 }
2517
2518 #[test]
2519 fn test_conv_list_types_exclude_private_all() {
2520 let args_with_types = vec![
2523 "slack".to_string(),
2524 "conv".to_string(),
2525 "list".to_string(),
2526 "--types=public_channel".to_string(),
2527 ];
2528 assert_eq!(
2529 get_option(&args_with_types, "--types="),
2530 Some("public_channel".to_string())
2531 );
2532
2533 let args_with_private = vec![
2534 "slack".to_string(),
2535 "conv".to_string(),
2536 "list".to_string(),
2537 "--types=public_channel".to_string(),
2538 "--include-private".to_string(),
2539 ];
2540 assert_eq!(
2541 get_option(&args_with_private, "--types="),
2542 Some("public_channel".to_string())
2543 );
2544 assert!(has_flag(&args_with_private, "--include-private"));
2545 }
2546
2547 #[test]
2548 fn test_conv_list_types_resolution_logic() {
2549 let args_no_flags = vec!["slack".to_string(), "conv".to_string(), "list".to_string()];
2551 let types = get_option(&args_no_flags, "--types=");
2552 let include_private = has_flag(&args_no_flags, "--include-private");
2553 let all = has_flag(&args_no_flags, "--all");
2554 assert!(types.is_none());
2555 assert!(!include_private);
2556 assert!(!all);
2557
2558 let args_private = vec![
2560 "slack".to_string(),
2561 "conv".to_string(),
2562 "list".to_string(),
2563 "--include-private".to_string(),
2564 ];
2565 let types = get_option(&args_private, "--types=");
2566 let include_private = has_flag(&args_private, "--include-private");
2567 let all = has_flag(&args_private, "--all");
2568 assert!(types.is_none());
2569 assert!(include_private);
2570 assert!(!all);
2571
2572 let args_all = vec![
2574 "slack".to_string(),
2575 "conv".to_string(),
2576 "list".to_string(),
2577 "--all".to_string(),
2578 ];
2579 let types = get_option(&args_all, "--types=");
2580 let include_private = has_flag(&args_all, "--include-private");
2581 let all = has_flag(&args_all, "--all");
2582 assert!(types.is_none());
2583 assert!(!include_private);
2584 assert!(all);
2585
2586 let args_conflict1 = vec![
2588 "slack".to_string(),
2589 "conv".to_string(),
2590 "list".to_string(),
2591 "--types=public_channel".to_string(),
2592 "--include-private".to_string(),
2593 ];
2594 let types = get_option(&args_conflict1, "--types=");
2595 let include_private = has_flag(&args_conflict1, "--include-private");
2596 assert!(types.is_some());
2597 assert!(include_private);
2598 let args_conflict2 = vec![
2602 "slack".to_string(),
2603 "conv".to_string(),
2604 "list".to_string(),
2605 "--types=public_channel".to_string(),
2606 "--all".to_string(),
2607 ];
2608 let types = get_option(&args_conflict2, "--types=");
2609 let all = has_flag(&args_conflict2, "--all");
2610 assert!(types.is_some());
2611 assert!(all);
2612 }
2614}