Skip to main content

agentzero_channels/channels/
channel_setup.rs

1#[allow(unused_imports)]
2use crate::ChannelRegistry;
3use serde::Deserialize;
4use std::collections::HashMap;
5#[allow(unused_imports)]
6use std::sync::Arc;
7
8/// Per-channel instance config from TOML `[channels.<name>]` sections.
9/// Uses a common structure with optional fields; each channel type consumes
10/// only the fields it needs.
11#[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    /// Per-channel privacy boundary override.
42    /// Empty string means inherit from `[channels] default_privacy_boundary`.
43    #[serde(default)]
44    pub privacy_boundary: String,
45}
46
47/// Register channels into `registry` based on the provided per-channel configs.
48///
49/// Each entry in `configs` maps a channel name (e.g. `"telegram"`) to its
50/// [`ChannelInstanceConfig`]. Only channels whose feature is compiled in
51/// will be registered; others are silently skipped.
52///
53/// Returns a list of `(channel_name, error)` for channels that failed to construct.
54pub 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/// Try to register a single channel.
79/// Returns `Ok(true)` if registered, `Ok(false)` if feature not compiled in,
80/// `Err(msg)` if config is invalid.
81#[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}