Skip to main content

greentic_setup/webhook/
mod.rs

1//! Webhook registration for messaging providers during setup.
2//!
3//! Provider packs implement the `setup_webhook` WASM operation to register
4//! webhooks with external APIs (e.g. Telegram `setWebhook`, Slack manifest
5//! update, Webex webhook management). The setup engine invokes this operation
6//! generically via `invoke_provider_op("setup_webhook", ...)`.
7//!
8//! This module provides the `register_webhook` entry point which checks for
9//! declared ops in config before falling back to the provider WASM operation.
10
11mod instructions;
12
13use serde_json::{Value, json};
14
15// Re-export public functions from submodules
16pub use instructions::{
17    ProviderInstruction, collect_post_setup_instructions, print_post_setup_instructions,
18};
19
20/// Extract registration result from declared ops in config.
21///
22/// If the config contains `webhook_ops`, return a result indicating
23/// declared ops mode instead of performing live registration.
24pub fn registration_result_from_declared_ops(config: &Value) -> Option<Value> {
25    let webhook_ops = config.get("webhook_ops")?.as_array()?;
26    if webhook_ops.is_empty() {
27        return None;
28    }
29    let subscription_ops = config
30        .get("subscription_ops")
31        .and_then(Value::as_array)
32        .cloned()
33        .unwrap_or_default();
34    let oauth_ops = config
35        .get("oauth_ops")
36        .and_then(Value::as_array)
37        .cloned()
38        .unwrap_or_default();
39
40    Some(json!({
41        "ok": true,
42        "mode": "declared_ops",
43        "webhook_ops": webhook_ops,
44        "subscription_ops": subscription_ops,
45        "oauth_ops": oauth_ops,
46    }))
47}
48
49/// Check whether a provider's answers contain a valid `public_base_url`
50/// suitable for webhook registration.
51pub fn has_webhook_url(answers: &Value) -> Option<&str> {
52    answers
53        .as_object()?
54        .get("public_base_url")?
55        .as_str()
56        .filter(|url| !url.is_empty() && url.starts_with("https://"))
57}
58
59/// Register a webhook for a provider based on its setup answers.
60///
61/// Returns `Some(result)` with status JSON from declared ops, or `None` if
62/// the provider doesn't declare webhook ops in its config. The actual webhook
63/// registration is handled by the provider's `setup_webhook` WASM operation,
64/// invoked by the setup engine after this function returns `None`.
65pub fn register_webhook(
66    _provider_id: &str,
67    config: &Value,
68    _tenant: &str,
69    _team: Option<&str>,
70) -> Option<Value> {
71    // Use declared ops from provider setup flow output if available
72    registration_result_from_declared_ops(config)
73}
74
75/// Build the webhook URL for a provider.
76pub fn build_webhook_url(
77    public_base_url: &str,
78    provider_id: &str,
79    tenant: &str,
80    team: &str,
81) -> String {
82    format!(
83        "{}/v1/messaging/ingress/{}/{}/{}",
84        public_base_url.trim_end_matches('/'),
85        provider_id,
86        tenant,
87        team,
88    )
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn has_webhook_url_valid() {
97        let config = json!({"public_base_url": "https://example.com"});
98        assert_eq!(has_webhook_url(&config), Some("https://example.com"));
99    }
100
101    #[test]
102    fn registration_result_from_declared_ops_uses_declared_ops() {
103        let config = json!({
104            "webhook_ops": [{"op": "register", "url": "https://example.com/webhook"}],
105            "subscription_ops": [{"op": "sync"}],
106            "oauth_ops": []
107        });
108        let result =
109            registration_result_from_declared_ops(&config).expect("declared ops registration");
110        assert_eq!(result["ok"], Value::Bool(true));
111        assert_eq!(result["mode"], Value::String("declared_ops".to_string()));
112        assert_eq!(
113            result["webhook_ops"][0]["op"],
114            Value::String("register".to_string())
115        );
116    }
117
118    #[test]
119    fn register_webhook_prefers_declared_ops() {
120        let config = json!({
121            "public_base_url": "http://example.com",
122            "webhook_ops": [{"op": "register", "url": "https://example.com/webhook"}]
123        });
124        let result = register_webhook("messaging-unknown", &config, "demo", None)
125            .expect("declared ops fallback");
126        assert_eq!(result["mode"], Value::String("declared_ops".to_string()));
127    }
128
129    #[test]
130    fn has_webhook_url_http_rejected() {
131        let config = json!({"public_base_url": "http://example.com"});
132        assert_eq!(has_webhook_url(&config), None);
133    }
134
135    #[test]
136    fn has_webhook_url_empty_rejected() {
137        let config = json!({"public_base_url": ""});
138        assert_eq!(has_webhook_url(&config), None);
139    }
140
141    #[test]
142    fn register_webhook_returns_none_without_declared_ops() {
143        let config = json!({"public_base_url": "https://example.com", "bot_token": "x"});
144        assert!(register_webhook("messaging-telegram", &config, "demo", None).is_none());
145    }
146
147    #[test]
148    fn register_webhook_returns_none_without_public_url() {
149        let config = json!({"bot_token": "x"});
150        assert!(register_webhook("messaging-telegram", &config, "demo", None).is_none());
151    }
152
153    #[test]
154    fn build_webhook_url_format() {
155        let url = build_webhook_url(
156            "https://example.com",
157            "messaging-telegram",
158            "demo",
159            "default",
160        );
161        assert_eq!(
162            url,
163            "https://example.com/v1/messaging/ingress/messaging-telegram/demo/default"
164        );
165    }
166
167    #[test]
168    fn build_webhook_url_trims_trailing_slash() {
169        let url = build_webhook_url(
170            "https://example.com/",
171            "messaging-telegram",
172            "demo",
173            "default",
174        );
175        assert_eq!(
176            url,
177            "https://example.com/v1/messaging/ingress/messaging-telegram/demo/default"
178        );
179    }
180}