1mod instructions;
12mod slack;
13mod telegram;
14mod webex;
15
16use serde_json::{Value, json};
17
18pub use instructions::{
20 ProviderInstruction, collect_post_setup_instructions, print_post_setup_instructions,
21};
22pub use slack::update_manifest_urls as slack_update_manifest_urls;
23
24pub fn registration_result_from_declared_ops(config: &Value) -> Option<Value> {
29 let webhook_ops = config.get("webhook_ops")?.as_array()?;
30 if webhook_ops.is_empty() {
31 return None;
32 }
33 let subscription_ops = config
34 .get("subscription_ops")
35 .and_then(Value::as_array)
36 .cloned()
37 .unwrap_or_default();
38 let oauth_ops = config
39 .get("oauth_ops")
40 .and_then(Value::as_array)
41 .cloned()
42 .unwrap_or_default();
43
44 Some(json!({
45 "ok": true,
46 "mode": "declared_ops",
47 "webhook_ops": webhook_ops,
48 "subscription_ops": subscription_ops,
49 "oauth_ops": oauth_ops,
50 }))
51}
52
53pub fn has_webhook_url(answers: &Value) -> Option<&str> {
56 answers
57 .as_object()?
58 .get("public_base_url")?
59 .as_str()
60 .filter(|url| !url.is_empty() && url.starts_with("https://"))
61}
62
63pub fn register_webhook(
69 provider_id: &str,
70 config: &Value,
71 tenant: &str,
72 team: Option<&str>,
73) -> Option<Value> {
74 if let Some(result) = registration_result_from_declared_ops(config) {
75 return Some(result);
76 }
77
78 let public_base_url = config.get("public_base_url").and_then(Value::as_str)?;
79 if public_base_url.is_empty() || !public_base_url.starts_with("https://") {
80 return None;
81 }
82
83 let team = team.unwrap_or("default");
84
85 let provider_short = provider_id
86 .strip_prefix("messaging-")
87 .unwrap_or(provider_id);
88
89 match provider_short {
90 "telegram" => {
91 telegram::setup_telegram_webhook(config, public_base_url, provider_id, tenant, team)
92 }
93 "slack" => slack::setup_slack_manifest(config, public_base_url, provider_id, tenant, team),
94 "webex" => webex::setup_webex_webhook(config, public_base_url, provider_id, tenant, team),
95 _ => None,
96 }
97}
98
99pub(crate) fn build_webhook_url(
101 public_base_url: &str,
102 provider_id: &str,
103 tenant: &str,
104 team: &str,
105) -> String {
106 format!(
107 "{}/v1/messaging/ingress/{}/{}/{}",
108 public_base_url.trim_end_matches('/'),
109 provider_id,
110 tenant,
111 team,
112 )
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn has_webhook_url_valid() {
121 let config = json!({"public_base_url": "https://example.com"});
122 assert_eq!(has_webhook_url(&config), Some("https://example.com"));
123 }
124
125 #[test]
126 fn registration_result_from_declared_ops_uses_declared_ops() {
127 let config = json!({
128 "webhook_ops": [{"op": "register", "url": "https://example.com/webhook"}],
129 "subscription_ops": [{"op": "sync"}],
130 "oauth_ops": []
131 });
132 let result =
133 registration_result_from_declared_ops(&config).expect("declared ops registration");
134 assert_eq!(result["ok"], Value::Bool(true));
135 assert_eq!(result["mode"], Value::String("declared_ops".to_string()));
136 assert_eq!(
137 result["webhook_ops"][0]["op"],
138 Value::String("register".to_string())
139 );
140 }
141
142 #[test]
143 fn register_webhook_prefers_declared_ops() {
144 let config = json!({
145 "public_base_url": "http://example.com",
146 "webhook_ops": [{"op": "register", "url": "https://example.com/webhook"}]
147 });
148 let result = register_webhook("messaging-unknown", &config, "demo", None)
149 .expect("declared ops fallback");
150 assert_eq!(result["mode"], Value::String("declared_ops".to_string()));
151 assert_eq!(
152 result["webhook_ops"][0]["url"],
153 "https://example.com/webhook"
154 );
155 }
156
157 #[test]
158 fn has_webhook_url_http_rejected() {
159 let config = json!({"public_base_url": "http://example.com"});
160 assert_eq!(has_webhook_url(&config), None);
161 }
162
163 #[test]
164 fn has_webhook_url_empty_rejected() {
165 let config = json!({"public_base_url": ""});
166 assert_eq!(has_webhook_url(&config), None);
167 }
168
169 #[test]
170 fn register_webhook_skips_unknown_provider() {
171 let config = json!({"public_base_url": "https://example.com", "bot_token": "x"});
172 assert!(register_webhook("messaging-unknown", &config, "demo", None).is_none());
173 }
174
175 #[test]
176 fn register_webhook_skips_without_public_url() {
177 let config = json!({"bot_token": "x"});
178 assert!(register_webhook("messaging-telegram", &config, "demo", None).is_none());
179 }
180
181 #[test]
182 fn register_webhook_skips_http_url() {
183 let config = json!({"public_base_url": "http://example.com", "bot_token": "x"});
184 assert!(register_webhook("messaging-telegram", &config, "demo", None).is_none());
185 }
186
187 #[test]
188 fn build_webhook_url_format() {
189 let url = build_webhook_url(
190 "https://example.com",
191 "messaging-telegram",
192 "demo",
193 "default",
194 );
195 assert_eq!(
196 url,
197 "https://example.com/v1/messaging/ingress/messaging-telegram/demo/default"
198 );
199 }
200
201 #[test]
202 fn build_webhook_url_trims_trailing_slash() {
203 let url = build_webhook_url(
204 "https://example.com/",
205 "messaging-telegram",
206 "demo",
207 "default",
208 );
209 assert_eq!(
210 url,
211 "https://example.com/v1/messaging/ingress/messaging-telegram/demo/default"
212 );
213 }
214
215 #[test]
216 fn slack_skips_without_credentials() {
217 let config = json!({"public_base_url": "https://example.com"});
218 assert!(register_webhook("messaging-slack", &config, "demo", None).is_none());
219 }
220
221 #[test]
222 fn webex_skips_without_token() {
223 let config = json!({"public_base_url": "https://example.com"});
224 assert!(register_webhook("messaging-webex", &config, "demo", None).is_none());
225 }
226
227 #[test]
228 fn slack_update_manifest_urls_creates_settings() {
229 let mut manifest = json!({});
230 slack_update_manifest_urls(&mut manifest, "https://example.com/webhook");
231 let settings = manifest.get("settings").unwrap();
232 assert_eq!(
233 settings["event_subscriptions"]["request_url"],
234 "https://example.com/webhook"
235 );
236 assert_eq!(
237 settings["interactivity"]["request_url"],
238 "https://example.com/webhook"
239 );
240 assert_eq!(settings["interactivity"]["is_enabled"], true);
241 }
242
243 #[test]
244 fn slack_update_manifest_urls_updates_existing() {
245 let mut manifest = json!({
246 "settings": {
247 "event_subscriptions": { "request_url": "https://old.com" },
248 "interactivity": { "request_url": "https://old.com", "is_enabled": false }
249 }
250 });
251 slack_update_manifest_urls(&mut manifest, "https://new.com/webhook");
252 let settings = manifest.get("settings").unwrap();
253 assert_eq!(
254 settings["event_subscriptions"]["request_url"],
255 "https://new.com/webhook"
256 );
257 assert_eq!(
258 settings["interactivity"]["request_url"],
259 "https://new.com/webhook"
260 );
261 assert_eq!(settings["interactivity"]["is_enabled"], true);
262 }
263}