Skip to main content

greentic_setup/webhook/
instructions.rs

1//! Post-setup instructions for messaging providers.
2//!
3//! Some providers (e.g. Teams, WhatsApp) cannot be fully automated and require
4//! the user to complete additional steps in external portals.
5
6use serde_json::Value;
7
8/// A single provider's manual setup instructions.
9#[derive(Debug, Clone, serde::Serialize)]
10pub struct ProviderInstruction {
11    pub provider_name: String,
12    pub steps: Vec<String>,
13}
14
15/// Collect post-setup instructions for providers that need manual intervention.
16///
17/// Returns structured data that can be displayed in terminal or UI.
18pub fn collect_post_setup_instructions(
19    providers: &[(String, Value)],
20    tenant: &str,
21    team: &str,
22) -> Vec<ProviderInstruction> {
23    let mut instructions: Vec<(&str, Vec<String>)> = Vec::new();
24
25    for (provider_id, config) in providers {
26        let provider_short = provider_id
27            .strip_prefix("messaging-")
28            .unwrap_or(provider_id);
29
30        let public_base_url = config
31            .get("public_base_url")
32            .and_then(Value::as_str)
33            .unwrap_or("<your-public-url>");
34
35        match provider_short {
36            "teams" => {
37                let webhook_url = format!(
38                    "{}/v1/messaging/ingress/{}/{}/{}",
39                    public_base_url.trim_end_matches('/'),
40                    provider_id,
41                    tenant,
42                    team,
43                );
44                instructions.push(("Microsoft Teams", vec![
45                    "1. Go to Azure Portal → Bot Services → your bot".into(),
46                    format!("2. Set Messaging Endpoint to: {webhook_url}"),
47                    "3. Ensure the App ID and Password match your answers file".into(),
48                    "4. Grant API permissions (delegated): Channel.ReadBasic.All, ChannelMessage.Send, Team.ReadBasic.All, ChatMessage.Send".into(),
49                    "5. If using Graph API: complete OAuth flow to obtain a refresh token".into(),
50                    "   → See: docs/guides/providers/guide-teams-setup.md".into(),
51                ]));
52            }
53            "whatsapp" => {
54                let webhook_url = format!(
55                    "{}/v1/messaging/ingress/{}/{}/{}",
56                    public_base_url.trim_end_matches('/'),
57                    provider_id,
58                    tenant,
59                    team,
60                );
61                instructions.push((
62                    "WhatsApp",
63                    vec![
64                        "1. Go to Meta Developer Portal → WhatsApp → Configuration".into(),
65                        format!("2. Set Webhook URL to: {webhook_url}"),
66                        "3. Set Verify Token to match your config (if configured)".into(),
67                        "4. Subscribe to webhook fields: messages".into(),
68                    ],
69                ));
70            }
71            "webex" => {
72                // Webex webhooks are auto-registered, but mention bot creation
73                // Check both webex_bot_token (canonical from WEBEX_BOT_TOKEN) and bot_token (QA field)
74                let webex_token = config.get("webex_bot_token");
75                let bot_token = config.get("bot_token");
76                eprintln!(
77                    "  [debug] webex instruction check: webex_bot_token={:?} bot_token={:?}",
78                    webex_token.map(|v| if v.as_str().map(|s| s.len()).unwrap_or(0) > 10 {
79                        "***"
80                    } else {
81                        "empty"
82                    }),
83                    bot_token.map(|v| if v.as_str().map(|s| s.len()).unwrap_or(0) > 10 {
84                        "***"
85                    } else {
86                        "empty"
87                    })
88                );
89                let has_token = webex_token
90                    .or(bot_token)
91                    .and_then(Value::as_str)
92                    .is_some_and(|s| !s.is_empty());
93                if !has_token {
94                    instructions.push((
95                        "Webex",
96                        vec![
97                            "1. Create a Webex Bot at: https://developer.webex.com/my-apps/new/bot"
98                                .into(),
99                            "2. Copy the bot access token into your answers file as 'webex_bot_token'"
100                                .into(),
101                            "3. Re-run setup to register webhooks automatically".into(),
102                        ],
103                    ));
104                }
105            }
106            "slack" => {
107                // Slack manifest is auto-updated, but mention app creation
108                let has_app_id = config
109                    .get("slack_app_id")
110                    .and_then(Value::as_str)
111                    .is_some_and(|s| !s.is_empty());
112                if !has_app_id {
113                    instructions.push(("Slack", vec![
114                        "1. Create a Slack App at: https://api.slack.com/apps".into(),
115                        "2. Add 'slack_app_id' and 'slack_configuration_token' to your answers file".into(),
116                        "3. Re-run setup to update the app manifest automatically".into(),
117                    ]));
118                }
119            }
120            _ => {}
121        }
122    }
123
124    instructions
125        .into_iter()
126        .map(|(name, steps)| ProviderInstruction {
127            provider_name: name.to_string(),
128            steps,
129        })
130        .collect()
131}
132
133/// Print post-setup instructions for providers that need manual intervention.
134///
135/// Some providers (e.g. Teams, WhatsApp) cannot be fully automated and require
136/// the user to complete additional steps in external portals.
137pub fn print_post_setup_instructions(providers: &[(String, Value)], tenant: &str, team: &str) {
138    let instructions = collect_post_setup_instructions(providers, tenant, team);
139
140    if instructions.is_empty() {
141        return;
142    }
143
144    println!();
145    println!("──────────────────────────────────────────────────────────");
146    println!("  Manual steps required:");
147    println!("──────────────────────────────────────────────────────────");
148    for instr in &instructions {
149        println!();
150        println!("  [{}]", instr.provider_name);
151        for step in &instr.steps {
152            println!("    {step}");
153        }
154    }
155    println!();
156    println!("──────────────────────────────────────────────────────────");
157}