1use std::collections::HashMap;
6use zeroclaw_config::schema::Config;
7
8macro_rules! scan_channel_map {
20 ($entries:expr, push, $channel_type:ident, $map:expr) => {
21 for (alias, cfg) in $map {
22 if cfg.enabled
23 && let Some(ref t) = cfg.default_target
24 {
25 let key = if alias.is_empty() {
26 stringify!($channel_type).to_string()
27 } else {
28 format!(concat!(stringify!($channel_type), ".{}"), alias)
29 };
30 $entries.push((key, t.clone()));
31 }
32 }
33 };
34 ($map_out:expr, insert, $channel_type:ident, $map:expr) => {
35 for (alias, cfg) in $map {
36 if cfg.enabled
37 && let Some(ref t) = cfg.default_target
38 {
39 let key = if alias.is_empty() {
40 stringify!($channel_type).to_string()
41 } else {
42 format!(concat!(stringify!($channel_type), ".{}"), alias)
43 };
44 $map_out.insert(key, t.clone());
45 }
46 }
47 };
48}
49
50pub fn build_channel_targets(config: &Config) -> Option<String> {
51 let mut entries: Vec<(String, String)> = Vec::new();
52
53 scan_channel_map!(entries, push, telegram, &config.channels.telegram);
54 scan_channel_map!(entries, push, discord, &config.channels.discord);
55 scan_channel_map!(entries, push, slack, &config.channels.slack);
56 scan_channel_map!(entries, push, mattermost, &config.channels.mattermost);
57 scan_channel_map!(entries, push, matrix, &config.channels.matrix);
58 scan_channel_map!(entries, push, irc, &config.channels.irc);
59 scan_channel_map!(entries, push, signal, &config.channels.signal);
60 scan_channel_map!(entries, push, whatsapp, &config.channels.whatsapp);
61 scan_channel_map!(entries, push, lark, &config.channels.lark);
62
63 if entries.is_empty() {
64 return None;
65 }
66
67 let mut out = String::new();
68 out.push_str("## Configured Channel Targets\n\n");
69 out.push_str("Use these recipients when sending messages via the `channel_send` tool. For each entry, use the composite key (e.g. `telegram.default`) as the `channel` parameter and the target as the `to` parameter.\n\n");
70 for (channel, target) in &entries {
71 out.push_str(&format!("- {channel}: {target}\n"));
72 }
73 Some(out)
74}
75
76pub fn build_default_targets_map(config: &Config) -> HashMap<String, String> {
82 let mut map = HashMap::new();
83
84 scan_channel_map!(map, insert, telegram, &config.channels.telegram);
85 scan_channel_map!(map, insert, discord, &config.channels.discord);
86 scan_channel_map!(map, insert, slack, &config.channels.slack);
87 scan_channel_map!(map, insert, mattermost, &config.channels.mattermost);
88 scan_channel_map!(map, insert, matrix, &config.channels.matrix);
89 scan_channel_map!(map, insert, irc, &config.channels.irc);
90 scan_channel_map!(map, insert, signal, &config.channels.signal);
91 scan_channel_map!(map, insert, whatsapp, &config.channels.whatsapp);
92 scan_channel_map!(map, insert, lark, &config.channels.lark);
93
94 map
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::collections::HashMap;
101 use zeroclaw_config::schema::{
102 ChannelsConfig, Config, DiscordConfig, LarkConfig, TelegramConfig,
103 };
104
105 fn make_config() -> Config {
106 Config {
107 channels: ChannelsConfig::default(),
108 ..Config::default()
109 }
110 }
111
112 #[test]
113 fn returns_none_when_no_channels_configured() {
114 let config = make_config();
115 assert!(build_channel_targets(&config).is_none());
116 }
117
118 #[test]
119 fn returns_none_when_channels_have_no_default_target() {
120 let mut config = make_config();
121 let mut telegram = HashMap::new();
122 telegram.insert(
123 "default".to_string(),
124 TelegramConfig {
125 enabled: true,
126 bot_token: "test-token".to_string(),
127 ..TelegramConfig::default()
128 },
129 );
130 config.channels.telegram = telegram;
131 assert!(build_channel_targets(&config).is_none());
132 }
133
134 #[test]
135 fn returns_none_when_channel_disabled() {
136 let mut config = make_config();
137 let mut telegram = HashMap::new();
138 telegram.insert(
139 "default".to_string(),
140 TelegramConfig {
141 enabled: false,
142 bot_token: "test-token".to_string(),
143 default_target: Some("chat_123".to_string()),
144 ..TelegramConfig::default()
145 },
146 );
147 config.channels.telegram = telegram;
148 assert!(build_channel_targets(&config).is_none());
149 }
150
151 #[test]
152 fn returns_single_telegram_target() {
153 let mut config = make_config();
154 let mut telegram = HashMap::new();
155 telegram.insert(
156 "default".to_string(),
157 TelegramConfig {
158 enabled: true,
159 bot_token: "test-token".to_string(),
160 default_target: Some("chat_123".to_string()),
161 ..TelegramConfig::default()
162 },
163 );
164 config.channels.telegram = telegram;
165
166 let result = build_channel_targets(&config).unwrap();
167 assert!(result.contains("## Configured Channel Targets"));
168 assert!(result.contains("telegram.default: chat_123"));
169 }
170
171 #[test]
172 fn returns_multiple_channels_of_different_types() {
173 let mut config = make_config();
174
175 let mut telegram = HashMap::new();
176 telegram.insert(
177 "default".to_string(),
178 TelegramConfig {
179 enabled: true,
180 bot_token: "test-token".to_string(),
181 default_target: Some("tg_chat_1".to_string()),
182 ..TelegramConfig::default()
183 },
184 );
185 config.channels.telegram = telegram;
186
187 let mut discord = HashMap::new();
188 discord.insert(
189 "prod".to_string(),
190 DiscordConfig {
191 enabled: true,
192 bot_token: "test-token".to_string(),
193 default_target: Some("discord_chan_2".to_string()),
194 ..DiscordConfig::default()
195 },
196 );
197 config.channels.discord = discord;
198
199 let result = build_channel_targets(&config).unwrap();
200 assert!(result.contains("telegram.default: tg_chat_1"));
201 assert!(result.contains("discord.prod: discord_chan_2"));
202 }
203
204 #[test]
205 fn includes_lark_channel() {
206 let mut config = make_config();
207 let mut lark = HashMap::new();
208 lark.insert(
209 "default".to_string(),
210 LarkConfig {
211 enabled: true,
212 app_id: "test-app".to_string(),
213 app_secret: "test-secret".to_string(),
214 default_target: Some("lark_chat_42".to_string()),
215 ..LarkConfig::default()
216 },
217 );
218 config.channels.lark = lark;
219
220 let result = build_channel_targets(&config).unwrap();
221 assert!(result.contains("lark.default: lark_chat_42"));
222 }
223
224 #[test]
225 fn output_contains_usage_instructions() {
226 let mut config = make_config();
227 let mut telegram = HashMap::new();
228 telegram.insert(
229 "default".to_string(),
230 TelegramConfig {
231 enabled: true,
232 bot_token: "test-token".to_string(),
233 default_target: Some("chat_123".to_string()),
234 ..TelegramConfig::default()
235 },
236 );
237 config.channels.telegram = telegram;
238
239 let result = build_channel_targets(&config).unwrap();
240 assert!(result.contains("channel_send"));
241 assert!(result.contains("composite key"));
242 }
243
244 #[test]
245 fn default_targets_map_empty_when_no_channels() {
246 let config = make_config();
247 let map = build_default_targets_map(&config);
248 assert!(map.is_empty());
249 }
250
251 #[test]
252 fn default_targets_map_contains_configured_targets() {
253 let mut config = make_config();
254 let mut telegram = HashMap::new();
255 telegram.insert(
256 "default".to_string(),
257 TelegramConfig {
258 enabled: true,
259 bot_token: "test-token".to_string(),
260 default_target: Some("chat_123".to_string()),
261 ..TelegramConfig::default()
262 },
263 );
264 config.channels.telegram = telegram;
265
266 let map = build_default_targets_map(&config);
267 assert_eq!(map.get("telegram.default"), Some(&"chat_123".to_string()));
268 }
269}