Skip to main content

zeroclaw_channels/
listing.rs

1//! Enumerate the channel types compiled into this binary.
2//!
3//! Use [`compiled_channels`] in display commands (`zeroclaw channel list`) that
4//! should only mention channels that can actually be started.  For a full
5//! channel inventory regardless of compile-time features, use
6//! [`zeroclaw_config::schema::ChannelsConfig::channels`] instead.
7
8use zeroclaw_config::schema::ChannelsConfig;
9use zeroclaw_config::traits::ChannelInfo;
10
11struct ChannelCompileSpec {
12    /// Display name from `ChannelsConfig::channels()`, when the channel lives
13    /// in the schema channel inventory. ACP is configured under `[acp]`, so it
14    /// participates in type readiness without appearing in `compiled_channels`.
15    schema_name: Option<&'static str>,
16    /// Accepted config/API type keys. Include legacy underscore aliases where
17    /// earlier channel references allowed them.
18    type_keys: &'static [&'static str],
19    compiled: bool,
20}
21
22// Single source of truth for both display inventory and per-config type
23// readiness. Keep this schema-backed: tests assert enabled display names exist
24// in the config crate's canonical channel inventory and that keys do not drift.
25const CHANNEL_COMPILE_SPECS: &[ChannelCompileSpec] = &[
26    ChannelCompileSpec {
27        schema_name: Some("Telegram"),
28        type_keys: &["telegram"],
29        compiled: cfg!(feature = "channel-telegram"),
30    },
31    ChannelCompileSpec {
32        schema_name: Some("Discord"),
33        type_keys: &["discord"],
34        compiled: cfg!(feature = "channel-discord"),
35    },
36    ChannelCompileSpec {
37        schema_name: Some("Slack"),
38        type_keys: &["slack"],
39        compiled: cfg!(feature = "channel-slack"),
40    },
41    ChannelCompileSpec {
42        schema_name: Some("Mattermost"),
43        type_keys: &["mattermost"],
44        compiled: cfg!(feature = "channel-mattermost"),
45    },
46    ChannelCompileSpec {
47        schema_name: Some("iMessage"),
48        type_keys: &["imessage"],
49        compiled: cfg!(feature = "channel-imessage"),
50    },
51    ChannelCompileSpec {
52        schema_name: Some("Matrix"),
53        type_keys: &["matrix"],
54        compiled: cfg!(feature = "channel-matrix"),
55    },
56    ChannelCompileSpec {
57        schema_name: Some("Signal"),
58        type_keys: &["signal"],
59        compiled: cfg!(feature = "channel-signal"),
60    },
61    ChannelCompileSpec {
62        schema_name: Some("WhatsApp"),
63        type_keys: &["whatsapp"],
64        compiled: cfg!(feature = "channel-whatsapp-cloud"),
65    },
66    ChannelCompileSpec {
67        schema_name: Some("WhatsApp Web"),
68        type_keys: &["whatsapp-web", "whatsapp_web"],
69        compiled: cfg!(feature = "whatsapp-web"),
70    },
71    ChannelCompileSpec {
72        schema_name: Some("Linq"),
73        type_keys: &["linq"],
74        compiled: cfg!(feature = "channel-linq"),
75    },
76    ChannelCompileSpec {
77        schema_name: Some("WATI"),
78        type_keys: &["wati"],
79        compiled: cfg!(feature = "channel-wati"),
80    },
81    ChannelCompileSpec {
82        schema_name: Some("NextCloud Talk"),
83        type_keys: &["nextcloud-talk", "nextcloud_talk"],
84        compiled: cfg!(feature = "channel-nextcloud"),
85    },
86    ChannelCompileSpec {
87        schema_name: Some("Email"),
88        type_keys: &["email"],
89        compiled: cfg!(feature = "channel-email"),
90    },
91    ChannelCompileSpec {
92        schema_name: Some("Gmail Push"),
93        type_keys: &["gmail-push", "gmail_push"],
94        compiled: cfg!(feature = "channel-email"),
95    },
96    ChannelCompileSpec {
97        schema_name: Some("IRC"),
98        type_keys: &["irc"],
99        compiled: cfg!(feature = "channel-irc"),
100    },
101    ChannelCompileSpec {
102        schema_name: Some("Twitch"),
103        type_keys: &["twitch"],
104        compiled: cfg!(feature = "channel-twitch"),
105    },
106    ChannelCompileSpec {
107        schema_name: Some("Lark"),
108        type_keys: &["lark", "feishu"],
109        compiled: cfg!(feature = "channel-lark"),
110    },
111    ChannelCompileSpec {
112        schema_name: Some("DingTalk"),
113        type_keys: &["dingtalk"],
114        compiled: cfg!(feature = "channel-dingtalk"),
115    },
116    ChannelCompileSpec {
117        schema_name: Some("WeCom"),
118        type_keys: &["wecom"],
119        compiled: cfg!(feature = "channel-wecom"),
120    },
121    ChannelCompileSpec {
122        schema_name: Some("WeCom WebSocket"),
123        type_keys: &["wecom-ws", "wecom_ws"],
124        compiled: cfg!(feature = "channel-wecom-ws"),
125    },
126    ChannelCompileSpec {
127        schema_name: Some("WeChat"),
128        type_keys: &["wechat"],
129        compiled: cfg!(feature = "channel-wechat"),
130    },
131    ChannelCompileSpec {
132        schema_name: Some("QQ Official"),
133        type_keys: &["qq"],
134        compiled: cfg!(feature = "channel-qq"),
135    },
136    ChannelCompileSpec {
137        schema_name: Some("Nostr"),
138        type_keys: &["nostr"],
139        compiled: cfg!(feature = "channel-nostr"),
140    },
141    ChannelCompileSpec {
142        schema_name: Some("ClawdTalk"),
143        type_keys: &["clawdtalk"],
144        compiled: cfg!(feature = "channel-clawdtalk"),
145    },
146    ChannelCompileSpec {
147        schema_name: Some("Reddit"),
148        type_keys: &["reddit"],
149        compiled: cfg!(feature = "channel-reddit"),
150    },
151    ChannelCompileSpec {
152        schema_name: Some("Bluesky"),
153        type_keys: &["bluesky"],
154        compiled: cfg!(feature = "channel-bluesky"),
155    },
156    ChannelCompileSpec {
157        schema_name: Some("X/Twitter"),
158        type_keys: &["twitter"],
159        compiled: cfg!(feature = "channel-twitter"),
160    },
161    ChannelCompileSpec {
162        schema_name: Some("Mochat"),
163        type_keys: &["mochat"],
164        compiled: cfg!(feature = "channel-mochat"),
165    },
166    ChannelCompileSpec {
167        schema_name: Some("LINE"),
168        type_keys: &["line"],
169        compiled: cfg!(feature = "channel-line"),
170    },
171    ChannelCompileSpec {
172        schema_name: Some("Voice Call"),
173        type_keys: &["voice-call", "voice_call"],
174        compiled: cfg!(feature = "channel-voice-call"),
175    },
176    ChannelCompileSpec {
177        schema_name: Some("VoiceWake"),
178        type_keys: &["voice-wake", "voice_wake"],
179        compiled: cfg!(feature = "voice-wake"),
180    },
181    ChannelCompileSpec {
182        schema_name: Some("MQTT"),
183        type_keys: &["mqtt"],
184        compiled: cfg!(feature = "channel-mqtt"),
185    },
186    ChannelCompileSpec {
187        schema_name: Some("AMQP"),
188        type_keys: &["amqp"],
189        compiled: cfg!(feature = "channel-amqp"),
190    },
191    ChannelCompileSpec {
192        schema_name: Some("Webhook"),
193        type_keys: &["webhook"],
194        compiled: cfg!(feature = "channel-webhook"),
195    },
196    ChannelCompileSpec {
197        schema_name: None,
198        type_keys: &["acp-server", "acp_server"],
199        compiled: cfg!(feature = "channel-acp-server"),
200    },
201];
202
203fn compiled_channel_names() -> impl Iterator<Item = &'static str> {
204    CHANNEL_COMPILE_SPECS
205        .iter()
206        .filter(|spec| spec.compiled)
207        .filter_map(|spec| spec.schema_name)
208}
209
210/// Returns one entry per channel type compiled into this binary.
211///
212/// Filters the canonical channel list from [`zeroclaw_config::schema::ChannelsConfig::channels`] down to
213/// only those enabled at compile time via `channel-*` / `voice-wake` feature
214/// flags. Name, desc, and configured status come from the config crate's single
215/// source of truth; this function contributes only the compile-time filter.
216pub fn compiled_channels(cfg: &ChannelsConfig) -> Vec<ChannelInfo> {
217    cfg.channels()
218        .into_iter()
219        .filter(|info| compiled_channel_names().any(|name| name == info.name))
220        .collect()
221}
222
223/// Returns whether a schema channel type key is compiled into this binary.
224///
225/// Accepts both kebab-case keys emitted by the config schema and legacy
226/// underscore spellings used in channel references.
227pub fn is_channel_type_compiled(channel_type: &str) -> bool {
228    for spec in CHANNEL_COMPILE_SPECS {
229        if spec.type_keys.contains(&channel_type) {
230            return spec.compiled;
231        }
232    }
233    false
234}
235
236#[cfg(test)]
237mod tests {
238    use super::{CHANNEL_COMPILE_SPECS, ChannelsConfig};
239    use super::{compiled_channels, is_channel_type_compiled};
240    use std::collections::BTreeSet;
241
242    #[cfg(feature = "default-channels")]
243    #[test]
244    fn channel_type_compilation_tracks_enabled_features() {
245        assert!(is_channel_type_compiled("telegram"));
246        assert!(is_channel_type_compiled("email"));
247        assert!(is_channel_type_compiled("webhook"));
248        assert!(is_channel_type_compiled("acp-server"));
249        assert_eq!(
250            is_channel_type_compiled("nextcloud-talk"),
251            cfg!(feature = "channel-nextcloud")
252        );
253        assert_eq!(
254            is_channel_type_compiled("linq"),
255            cfg!(feature = "channel-linq")
256        );
257    }
258
259    #[test]
260    fn compiled_channel_names_are_schema_names() {
261        let cfg = ChannelsConfig::default();
262        let schema_names: BTreeSet<_> = cfg.channels().into_iter().map(|info| info.name).collect();
263
264        for name in CHANNEL_COMPILE_SPECS
265            .iter()
266            .filter_map(|spec| spec.schema_name)
267        {
268            assert!(
269                schema_names.contains(name),
270                "compiled channel name `{name}` is missing from ChannelsConfig::channels()"
271            );
272        }
273    }
274
275    #[test]
276    fn compiled_channels_match_expected_schema_names() {
277        let cfg = ChannelsConfig::default();
278        let actual: BTreeSet<_> = compiled_channels(&cfg)
279            .into_iter()
280            .map(|info| info.name)
281            .collect();
282        let expected: BTreeSet<_> = CHANNEL_COMPILE_SPECS
283            .iter()
284            .filter(|spec| spec.compiled)
285            .filter_map(|spec| spec.schema_name)
286            .collect();
287
288        assert_eq!(actual, expected);
289    }
290
291    #[test]
292    fn channel_type_compilation_matches_inventory_specs() {
293        for spec in CHANNEL_COMPILE_SPECS {
294            for key in spec.type_keys {
295                assert_eq!(
296                    is_channel_type_compiled(key),
297                    spec.compiled,
298                    "channel type key `{key}` drifted from its compile spec"
299                );
300            }
301        }
302
303        assert!(!is_channel_type_compiled("not-a-channel"));
304    }
305
306    #[test]
307    fn channel_compile_specs_do_not_duplicate_entries() {
308        let mut seen_names = BTreeSet::new();
309        let mut seen_keys = BTreeSet::new();
310
311        for spec in CHANNEL_COMPILE_SPECS {
312            if let Some(name) = spec.schema_name {
313                assert!(
314                    seen_names.insert(name),
315                    "compiled channel name `{name}` appears more than once"
316                );
317            }
318
319            for key in spec.type_keys {
320                assert!(
321                    seen_keys.insert(*key),
322                    "compiled channel type key `{key}` appears more than once"
323                );
324            }
325        }
326    }
327
328    #[test]
329    fn channel_compile_specs_cover_schema_channel_types() {
330        let out_of_scope = BTreeSet::from([
331            // Voice duplex is a gateway event-stream config surface, not a
332            // `zeroclaw-channels` Channel implementation with its own feature.
333            "voice_duplex",
334        ]);
335
336        for channel_type in zeroclaw_config::schema::v2::V3_CHANNEL_TYPES {
337            if out_of_scope.contains(channel_type) {
338                continue;
339            }
340            let kebab = channel_type.replace('_', "-");
341            assert!(
342                CHANNEL_COMPILE_SPECS.iter().any(|spec| spec
343                    .type_keys
344                    .iter()
345                    .any(|key| *key == *channel_type || *key == kebab)),
346                "schema channel type `{channel_type}` is missing from CHANNEL_COMPILE_SPECS"
347            );
348        }
349    }
350}