agentzero_channels/channels/
channel_setup.rs1#[allow(unused_imports)]
2use crate::ChannelRegistry;
3use serde::Deserialize;
4use std::collections::HashMap;
5#[allow(unused_imports)]
6use std::sync::Arc;
7
8#[derive(Debug, Clone, Default, Deserialize)]
12#[serde(default)]
13pub struct ChannelInstanceConfig {
14 pub bot_token: Option<String>,
15 pub app_token: Option<String>,
16 pub base_url: Option<String>,
17 pub token: Option<String>,
18 pub channel_id: Option<String>,
19 pub room_id: Option<String>,
20 pub homeserver: Option<String>,
21 pub access_token: Option<String>,
22 pub server: Option<String>,
23 pub port: Option<u16>,
24 pub nick: Option<String>,
25 pub channel_name: Option<String>,
26 pub password: Option<String>,
27 pub relay_url: Option<String>,
28 pub private_key_hex: Option<String>,
29 pub smtp_host: Option<String>,
30 pub smtp_port: Option<u16>,
31 pub imap_host: Option<String>,
32 pub imap_port: Option<u16>,
33 pub username: Option<String>,
34 pub from_address: Option<String>,
35 #[serde(default)]
36 pub allowed_users: Vec<String>,
37 #[serde(default)]
38 pub allowed_pubkeys: Vec<String>,
39 #[serde(default)]
40 pub allowed_senders: Vec<String>,
41 #[serde(default)]
44 pub privacy_boundary: String,
45}
46
47pub fn register_configured_channels(
55 registry: &mut ChannelRegistry,
56 configs: &HashMap<String, ChannelInstanceConfig>,
57) -> Vec<(String, String)> {
58 let mut errors = Vec::new();
59
60 for (name, config) in configs {
61 match register_one(registry, name, config) {
62 Ok(true) => {
63 tracing::info!(channel = %name, "registered configured channel");
64 }
65 Ok(false) => {
66 tracing::debug!(channel = %name, "channel not compiled in, skipping");
67 }
68 Err(e) => {
69 tracing::warn!(channel = %name, error = %e, "failed to register channel");
70 errors.push((name.clone(), e));
71 }
72 }
73 }
74
75 errors
76}
77
78#[allow(unused_variables)]
82fn register_one(
83 registry: &mut ChannelRegistry,
84 name: &str,
85 config: &ChannelInstanceConfig,
86) -> Result<bool, String> {
87 match name {
88 #[cfg(feature = "channel-telegram")]
89 "telegram" => {
90 let bot_token = config
91 .bot_token
92 .as_ref()
93 .ok_or("telegram requires bot_token")?;
94 let channel =
95 super::TelegramChannel::new(bot_token.clone(), config.allowed_users.clone());
96 registry.register(Arc::new(channel));
97 Ok(true)
98 }
99
100 #[cfg(feature = "channel-discord")]
101 "discord" => {
102 let bot_token = config
103 .bot_token
104 .as_ref()
105 .ok_or("discord requires bot_token")?;
106 let channel =
107 super::DiscordChannel::new(bot_token.clone(), config.allowed_users.clone());
108 registry.register(Arc::new(channel));
109 Ok(true)
110 }
111
112 #[cfg(feature = "channel-slack")]
113 "slack" => {
114 let bot_token = config
115 .bot_token
116 .as_ref()
117 .ok_or("slack requires bot_token")?;
118 let channel = super::SlackChannel::new(
119 bot_token.clone(),
120 config.app_token.clone(),
121 config.channel_id.clone(),
122 config.allowed_users.clone(),
123 );
124 registry.register(Arc::new(channel));
125 Ok(true)
126 }
127
128 #[cfg(feature = "channel-mattermost")]
129 "mattermost" => {
130 let base_url = config
131 .base_url
132 .as_ref()
133 .ok_or("mattermost requires base_url")?;
134 let token = config.token.as_ref().ok_or("mattermost requires token")?;
135 let channel = super::MattermostChannel::new(
136 base_url.clone(),
137 token.clone(),
138 config.channel_id.clone(),
139 config.allowed_users.clone(),
140 );
141 registry.register(Arc::new(channel));
142 Ok(true)
143 }
144
145 #[cfg(feature = "channel-matrix")]
146 "matrix" => {
147 let homeserver = config
148 .homeserver
149 .as_ref()
150 .ok_or("matrix requires homeserver")?;
151 let access_token = config
152 .access_token
153 .as_ref()
154 .ok_or("matrix requires access_token")?;
155 let room_id = config.room_id.clone().unwrap_or_default();
156 let channel = super::MatrixChannel::new(
157 homeserver.clone(),
158 access_token.clone(),
159 room_id,
160 config.allowed_users.clone(),
161 );
162 registry.register(Arc::new(channel));
163 Ok(true)
164 }
165
166 #[cfg(feature = "channel-email")]
167 "email" => {
168 use super::email::EmailConfig;
169 let smtp_host = config
170 .smtp_host
171 .as_ref()
172 .ok_or("email requires smtp_host")?;
173 let imap_host = config
174 .imap_host
175 .as_ref()
176 .ok_or("email requires imap_host")?;
177 let username = config.username.as_ref().ok_or("email requires username")?;
178 let password = config.password.as_ref().ok_or("email requires password")?;
179 let from_address = config
180 .from_address
181 .as_ref()
182 .ok_or("email requires from_address")?;
183 let email_config = EmailConfig {
184 smtp_host: smtp_host.clone(),
185 smtp_port: config.smtp_port.unwrap_or(587),
186 imap_host: imap_host.clone(),
187 imap_port: config.imap_port.unwrap_or(993),
188 username: username.clone(),
189 password: password.clone(),
190 from_address: from_address.clone(),
191 allowed_senders: config.allowed_senders.clone(),
192 };
193 let channel = super::EmailChannel::new(email_config);
194 registry.register(Arc::new(channel));
195 Ok(true)
196 }
197
198 #[cfg(feature = "channel-irc")]
199 "irc" => {
200 let server = config.server.as_ref().ok_or("irc requires server")?;
201 let nick = config.nick.as_ref().ok_or("irc requires nick")?;
202 let channel_name = config
203 .channel_name
204 .as_ref()
205 .ok_or("irc requires channel_name")?;
206 let channel = super::IrcChannel::new(
207 server.clone(),
208 config.port.unwrap_or(6667),
209 nick.clone(),
210 channel_name.clone(),
211 config.password.clone(),
212 config.allowed_users.clone(),
213 );
214 registry.register(Arc::new(channel));
215 Ok(true)
216 }
217
218 #[cfg(feature = "channel-nostr")]
219 "nostr" => {
220 let relay_url = config
221 .relay_url
222 .as_ref()
223 .ok_or("nostr requires relay_url")?;
224 let private_key_hex = config
225 .private_key_hex
226 .as_ref()
227 .ok_or("nostr requires private_key_hex")?;
228 let channel = super::NostrChannel::new(
229 relay_url.clone(),
230 private_key_hex.clone(),
231 config.allowed_pubkeys.clone(),
232 );
233 registry.register(Arc::new(channel));
234 Ok(true)
235 }
236
237 _ => Ok(false),
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn empty_configs_registers_nothing() {
247 let mut registry = ChannelRegistry::new();
248 let configs = HashMap::new();
249 let errors = register_configured_channels(&mut registry, &configs);
250 assert!(errors.is_empty());
251 assert!(registry.channel_names().is_empty());
252 }
253
254 #[test]
255 fn unknown_channel_is_silently_skipped() {
256 let mut registry = ChannelRegistry::new();
257 let mut configs = HashMap::new();
258 configs.insert(
259 "nonexistent-channel".to_string(),
260 ChannelInstanceConfig::default(),
261 );
262 let errors = register_configured_channels(&mut registry, &configs);
263 assert!(errors.is_empty());
264 assert!(!registry.has_channel("nonexistent-channel"));
265 }
266
267 #[cfg(feature = "channel-telegram")]
268 #[test]
269 fn telegram_missing_bot_token_returns_error() {
270 let mut registry = ChannelRegistry::new();
271 let mut configs = HashMap::new();
272 configs.insert("telegram".to_string(), ChannelInstanceConfig::default());
273 let errors = register_configured_channels(&mut registry, &configs);
274 assert_eq!(errors.len(), 1);
275 assert!(errors[0].1.contains("bot_token"));
276 }
277
278 #[cfg(feature = "channel-telegram")]
279 #[test]
280 fn telegram_with_bot_token_registers() {
281 let mut registry = ChannelRegistry::new();
282 let mut configs = HashMap::new();
283 configs.insert(
284 "telegram".to_string(),
285 ChannelInstanceConfig {
286 bot_token: Some("fake-token".into()),
287 ..Default::default()
288 },
289 );
290 let errors = register_configured_channels(&mut registry, &configs);
291 assert!(errors.is_empty());
292 assert!(registry.has_channel("telegram"));
293 }
294
295 #[test]
296 fn channel_instance_config_privacy_boundary_defaults_empty() {
297 let cfg = ChannelInstanceConfig::default();
298 assert_eq!(cfg.privacy_boundary, "");
299 }
300
301 #[test]
302 fn channel_instance_config_with_privacy_boundary() {
303 let cfg = ChannelInstanceConfig {
304 privacy_boundary: "local_only".to_string(),
305 ..Default::default()
306 };
307 assert_eq!(cfg.privacy_boundary, "local_only");
308 }
309}