apollo/channels/
registry.rs1use std::collections::HashMap;
11use std::sync::Arc;
12
13use super::traits::Channel;
14
15#[derive(Debug, Default, Clone)]
21pub struct ChannelSettings {
22 pub token: Option<String>,
23 pub values: HashMap<String, String>,
24}
25
26impl ChannelSettings {
27 pub fn new(token: Option<String>, values: HashMap<String, String>) -> Self {
28 Self { token, values }
29 }
30
31 pub fn get(&self, key: &str) -> Option<String> {
33 if let Some(v) = self.values.get(key) {
34 return Some(v.clone());
35 }
36 std::env::var(format!("APOLLO_CHANNEL_{}", key.to_uppercase())).ok()
37 }
38
39 pub fn require(&self, key: &str) -> anyhow::Result<String> {
42 self.get(key).ok_or_else(|| {
43 anyhow::anyhow!(
44 "channel setting `{key}` is required — set [channel].settings.{key} \
45 in the config or APOLLO_CHANNEL_{} in the environment",
46 key.to_uppercase()
47 )
48 })
49 }
50
51 pub fn token(&self) -> anyhow::Result<String> {
54 if let Some(t) = &self.token {
55 return Ok(t.clone());
56 }
57 self.require("token")
58 }
59}
60
61pub type ChannelBuilder =
63 Arc<dyn Fn(&ChannelSettings) -> anyhow::Result<Box<dyn Channel>> + Send + Sync>;
64
65#[derive(Clone, Default)]
67pub struct ChannelRegistry {
68 builders: HashMap<String, ChannelBuilder>,
69}
70
71impl ChannelRegistry {
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn with_builtins() -> Self {
78 let mut reg = Self::new();
79
80 #[cfg(feature = "channel-cli")]
81 reg.register("cli", |_| Ok(Box::new(super::cli::CliChannel::new())));
82
83 #[cfg(feature = "channel-telegram")]
84 reg.register("telegram", |s| {
85 let chat_id: i64 = s.require("chat_id")?.parse()?;
86 Ok(Box::new(super::telegram::TelegramChannel::new(
87 s.token()?,
88 chat_id,
89 )))
90 });
91
92 #[cfg(feature = "channel-discord")]
93 reg.register("discord", |s| {
94 Ok(Box::new(super::discord::DiscordChannel::new(
95 s.token()?,
96 s.require("channel_id")?,
97 )))
98 });
99
100 #[cfg(feature = "channel-slack")]
101 reg.register("slack", |s| {
102 Ok(Box::new(
103 super::slack::SlackChannel::new(s.token()?).with_channel(s.require("channel_id")?),
104 ))
105 });
106
107 #[cfg(feature = "channel-matrix")]
108 reg.register("matrix", |s| {
109 Ok(Box::new(
110 super::matrix::MatrixChannel::new(s.require("homeserver")?, s.token()?)
111 .with_room(s.require("room")?),
112 ))
113 });
114
115 #[cfg(feature = "channel-irc")]
116 reg.register("irc", |s| {
117 let mut ch = super::irc::IrcChannel::new(
118 s.require("server")?,
119 s.require("channel")?,
120 s.get("nick").unwrap_or_else(|| "apollo".to_string()),
121 );
122 if let Some(port) = s.get("port") {
123 ch = ch.with_port(port.parse()?);
124 }
125 if let Some(password) = s.get("password") {
126 ch = ch.with_password(password);
127 }
128 Ok(Box::new(ch))
129 });
130
131 #[cfg(feature = "channel-signal")]
132 reg.register("signal", |s| {
133 Ok(Box::new(super::signal::SignalChannel::new(
134 s.require("api_base")?,
135 s.require("number")?,
136 )))
137 });
138
139 #[cfg(feature = "channel-whatsapp")]
140 reg.register("whatsapp", |s| {
141 Ok(Box::new(super::whatsapp::WhatsAppChannel::new(
142 s.token()?,
143 s.require("phone_number_id")?,
144 s.get("verify_token")
145 .unwrap_or_else(|| "apollo-verify".to_string()),
146 )))
147 });
148
149 #[cfg(feature = "channel-googlechat")]
150 reg.register("googlechat", |s| {
151 Ok(Box::new(super::googlechat::GoogleChatChannel::new(
152 s.require("service_account_key")?,
153 )))
154 });
155
156 #[cfg(feature = "channel-msteams")]
157 reg.register("msteams", |s| {
158 Ok(Box::new(super::msteams::TeamsChannel::new(
159 s.require("app_id")?,
160 s.require("app_password")?,
161 )))
162 });
163
164 reg
165 }
166
167 pub fn register<F>(&mut self, name: impl Into<String>, builder: F)
170 where
171 F: Fn(&ChannelSettings) -> anyhow::Result<Box<dyn Channel>> + Send + Sync + 'static,
172 {
173 self.builders.insert(name.into(), Arc::new(builder));
174 }
175
176 pub fn register_builder(&mut self, name: impl Into<String>, builder: ChannelBuilder) {
178 self.builders.insert(name.into(), builder);
179 }
180
181 pub fn contains(&self, name: &str) -> bool {
182 self.builders.contains_key(name)
183 }
184
185 pub fn names(&self) -> Vec<String> {
187 let mut names: Vec<String> = self.builders.keys().cloned().collect();
188 names.sort();
189 names
190 }
191
192 pub fn build(
194 &self,
195 name: &str,
196 settings: &ChannelSettings,
197 ) -> anyhow::Result<Box<dyn Channel>> {
198 let builder = self.builders.get(name).ok_or_else(|| {
199 anyhow::anyhow!(
200 "unknown channel `{name}` (available: {})",
201 self.names().join(", ")
202 )
203 })?;
204 builder(settings)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn builtins_are_registered_and_named() {
214 let reg = ChannelRegistry::with_builtins();
215 for name in reg.names() {
218 assert!(reg.contains(&name), "names() listed unbuildable {name}");
219 }
220 #[cfg(feature = "channel-cli")]
221 assert!(reg.contains("cli"));
222 #[cfg(feature = "channel-slack")]
223 assert!(reg.contains("slack"), "slack must be reachable by name");
224 }
225
226 #[test]
227 fn unknown_channel_lists_the_alternatives() {
228 let reg = ChannelRegistry::with_builtins();
229 let err = match reg.build("nope", &ChannelSettings::default()) {
230 Err(e) => e.to_string(),
231 Ok(_) => panic!("built a channel that was never registered"),
232 };
233 assert!(err.contains("unknown channel `nope`"), "{err}");
234 assert!(err.contains("available:"), "{err}");
235 }
236
237 #[test]
238 fn missing_required_setting_names_both_sources() {
239 let err = ChannelSettings::default()
240 .require("channel_id")
241 .unwrap_err()
242 .to_string();
243 assert!(err.contains("[channel].settings.channel_id"), "{err}");
244 assert!(err.contains("APOLLO_CHANNEL_CHANNEL_ID"), "{err}");
245 }
246
247 #[test]
248 fn a_plugin_channel_can_replace_a_builtin() {
249 let mut reg = ChannelRegistry::with_builtins();
250 let before = reg.names().len();
251 reg.register("custom", |_| {
252 anyhow::bail!("constructed");
253 });
254 assert_eq!(reg.names().len(), before + 1);
255 assert!(reg.contains("custom"));
256 }
257}