1use serde_json::Value;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum DebugLevel {
12 Off,
14 Debug,
16 Trace,
18}
19
20pub fn get_debug_level(args: &[String]) -> DebugLevel {
28 if args.iter().any(|arg| arg == "--trace") {
30 return DebugLevel::Trace;
31 }
32
33 if args.iter().any(|arg| arg == "--debug") {
35 return DebugLevel::Debug;
36 }
37
38 if enabled() {
40 return DebugLevel::Debug;
41 }
42
43 DebugLevel::Off
44}
45
46pub fn enabled() -> bool {
50 match std::env::var("SLACK_RS_DEBUG") {
51 Ok(v) => {
52 let v = v.trim().to_ascii_lowercase();
53 matches!(v.as_str(), "1" | "true" | "yes" | "on")
54 }
55 Err(_) => false,
56 }
57}
58
59pub fn log(msg: impl AsRef<str>) {
61 if enabled() {
62 eprintln!("DEBUG: {}", msg.as_ref());
63 }
64}
65
66pub fn log_api_context(
71 level: DebugLevel,
72 profile_name: Option<&str>,
73 token_store_backend: &str,
74 token_type: &str,
75 method: &str,
76 endpoint: &str,
77) {
78 if level >= DebugLevel::Debug {
79 eprintln!("DEBUG: Profile: {}", profile_name.unwrap_or("<none>"));
80 eprintln!("DEBUG: Token store: {}", token_store_backend);
81 eprintln!("DEBUG: Token type: {}", token_type);
82 eprintln!("DEBUG: API method: {}", method);
83 eprintln!("DEBUG: Endpoint: {}", endpoint);
84 }
85}
86
87pub fn log_trace(level: DebugLevel, msg: impl AsRef<str>) {
91 if level >= DebugLevel::Trace {
92 eprintln!("TRACE: {}", msg.as_ref());
93 }
94}
95
96pub fn log_error_code(level: DebugLevel, response: &Value) {
100 if level >= DebugLevel::Debug {
101 if let Some(ok) = response.get("ok").and_then(|v| v.as_bool()) {
102 if !ok {
103 if let Some(error_code) = response.get("error").and_then(|v| v.as_str()) {
104 eprintln!("DEBUG: Slack error code: {}", error_code);
105 }
106 }
107 }
108 }
109}
110
111pub fn token_hint(token: &str) -> String {
115 let kind = if token.starts_with("xoxb-") {
116 "xoxb"
117 } else if token.starts_with("xoxp-") {
118 "xoxp"
119 } else if token.starts_with("xoxa-") {
120 "xoxa"
121 } else if token.starts_with("xoxr-") {
122 "xoxr"
123 } else if token.starts_with("xoxs-") {
124 "xoxs"
125 } else {
126 "token"
127 };
128
129 format!("{} (len={})", kind, token.len())
130}
131
132pub fn redact_json_secrets(json: &str) -> String {
137 let Ok(mut v) = serde_json::from_str::<Value>(json) else {
138 return "<non-json body>".to_string();
139 };
140
141 redact_value_in_place(&mut v);
142 serde_json::to_string(&v).unwrap_or_else(|_| "<unserializable json>".to_string())
143}
144
145fn redact_value_in_place(v: &mut Value) {
146 match v {
147 Value::Object(map) => {
148 for (_k, child) in map.iter_mut() {
149 redact_value_in_place(child);
150 }
151 }
152 Value::Array(items) => {
153 for child in items.iter_mut() {
154 redact_value_in_place(child);
155 }
156 }
157 Value::String(s) => {
158 let trimmed = s.trim();
159 if trimmed.starts_with("xox") {
160 *s = "<redacted>".to_string();
161 }
162 }
163 _ => {}
164 }
165}