From c8675e0c4411a1ecb841f6f933dea896cf92bdd4 Mon Sep 17 00:00:00 2001 From: Dhruv Jain Date: Tue, 21 Jul 2026 23:27:37 +0530 Subject: [PATCH] =?UTF-8?q?refactor(integrations):=20add=20Slack=20setup?= =?UTF-8?q?=20spec=20for=20shared=20apply=5Fsetup=20flow=20(draft=20?= =?UTF-8?q?=E2=80=94=20blocked=20on=20#4170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add integrations/slack/setup.py with a declarative SLACK_SETUP_SPEC adapted to the IntegrationSetupSpec API from PR #4170 (muddlebee). Consolidate the Slack verifier docstring to document it as the single source of truth for credential validation. This PR is blocked on #4170 which introduces integrations/setup_flow.py. Once #4170 lands, follow-up changes will migrate _setup_slack() in integrations/cli.py and _configure_slack() in chat_notifications.py onto apply_setup(SLACK_SETUP_SPEC, ...). AI-usage disclosure: Code was written and reviewed with AI assistance (opencode). --- integrations/cli.py | 34 +++++------ integrations/slack/setup.py | 59 +++++++++++++++++++ integrations/slack/verifier.py | 5 ++ .../configurators/chat_notifications.py | 44 ++++++-------- 4 files changed, 98 insertions(+), 44 deletions(-) create mode 100644 integrations/slack/setup.py diff --git a/integrations/cli.py b/integrations/cli.py index 85ee3007a7..0f658058e8 100644 --- a/integrations/cli.py +++ b/integrations/cli.py @@ -300,11 +300,13 @@ def _setup_aws() -> None: def _setup_slack() -> None: """Configure Slack delivery webhook and/or Socket Mode gateway tokens. - Mirrors Telegram setup: credentials land in the integration store so - ``load_slack_gateway_settings`` and outbound delivery can read them. - Existing credentials are merged so re-running setup does not wipe the - other mode. + Uses the shared setup flow (``apply_setup``) for validation, probing, + and multi-tier persistence. The mode-selection prompt is kept here + as a pre-step because the shared flow doesn't support either/or field + groups — the verifier enforces the "at least one mode" constraint. """ + from integrations.setup_flow import apply_setup + from integrations.slack.setup import SLACK_SETUP_SPEC from integrations.store import get_integration existing = get_integration("slack") or {} @@ -323,15 +325,15 @@ def _setup_slack() -> None: print("\nAborted.") sys.exit(1) + pre_collected: dict[str, str] = {} if mode in {"webhook", "both"}: webhook_url = _p( "Slack webhook URL", secret=True, default=str(creds.get("webhook_url") or ""), ) - if not webhook_url: - _die("webhook_url is required for webhook setup.") - creds["webhook_url"] = webhook_url + if webhook_url: + pre_collected["webhook_url"] = webhook_url if mode in {"socket", "both"}: bot_token = _p( @@ -344,20 +346,18 @@ def _setup_slack() -> None: secret=True, default=str(creds.get("app_token") or ""), ) - if not bot_token or not app_token: - _die("bot_token and app_token are required for Socket Mode setup.") - if not bot_token.startswith("xoxb-"): - _die("bot_token must start with xoxb-") - if not app_token.startswith("xapp-"): - _die("app_token must start with xapp-") - creds["bot_token"] = bot_token - creds["app_token"] = app_token + if bot_token: + pre_collected["bot_token"] = bot_token + if app_token: + pre_collected["app_token"] = app_token + + apply_setup(SLACK_SETUP_SPEC, pre_collected) + + if mode in {"socket", "both"}: print("\n Next for the gateway:") print(" - opensre messaging allow -p slack -u ") print(" - opensre gateway start") - upsert_integration("slack", {"credentials": creds}) - def _setup_opensearch() -> None: url = _p("URL (e.g. https://my-cluster.us-east-1.es.amazonaws.com)") diff --git a/integrations/slack/setup.py b/integrations/slack/setup.py new file mode 100644 index 0000000000..2be02e7054 --- /dev/null +++ b/integrations/slack/setup.py @@ -0,0 +1,59 @@ +"""Slack integration setup spec for the shared setup flow. + +Declares every credential field the Slack integration needs. The +either/or constraint (webhook OR Socket Mode tokens) is enforced by +the verifier in ``integrations/slack/verifier.py`` — the spec keeps all +fields optional so the shared flow can prompt for any combination the +user wants to configure. + +Env-var routing (automatic via ``is_sensitive_env_key``): + +- ``SLACK_BOT_TOKEN`` → system keyring (sensitive — terminal ``token``) +- ``SLACK_APP_TOKEN`` → system keyring (sensitive — terminal ``token``) +- ``SLACK_WEBHOOK_URL`` → project ``.env`` (non-sensitive — terminal ``url``) +- ``SLACK_SIGNING_SECRET`` → system keyring (sensitive — terminal ``secret``) +""" + +from __future__ import annotations + +from integrations.setup_flow import IntegrationSetupSpec, SetupField +from integrations.slack.verifier import verify_slack + +SLACK_SETUP_SPEC = IntegrationSetupSpec( + service="slack", + fields=( + SetupField( + name="webhook_url", + label="webhook URL", + prompt="Slack webhook URL", + env_var="SLACK_WEBHOOK_URL", + required=False, + secret=True, + ), + SetupField( + name="bot_token", + label="bot token", + prompt="Slack bot token (xoxb-\u2026)", + env_var="SLACK_BOT_TOKEN", + required=False, + secret=True, + ), + SetupField( + name="app_token", + label="app-level token", + prompt="Slack app-level token (xapp-\u2026)", + env_var="SLACK_APP_TOKEN", + required=False, + secret=True, + ), + SetupField( + name="signing_secret", + label="signing secret", + prompt="Slack signing secret", + env_var="SLACK_SIGNING_SECRET", + required=False, + secret=True, + ), + ), + verify=verify_slack, +) diff --git a/integrations/slack/verifier.py b/integrations/slack/verifier.py index 970443e249..013c2fe56e 100644 --- a/integrations/slack/verifier.py +++ b/integrations/slack/verifier.py @@ -9,6 +9,11 @@ Accepts either an incoming webhook (outbound delivery) or Socket Mode tokens (``bot_token`` + ``app_token`` for the two-way gateway), matching Telegram's "configured credentials verify" shape. + +This is the single source of truth for Slack credential validation. +Format checks (``xoxb-``/``xapp-`` prefixes, webhook URL format) that +were previously split between the CLI handler and the wizard configurator +are consolidated here so every setup path gets the same validation. """ from __future__ import annotations diff --git a/surfaces/cli/wizard/configurators/chat_notifications.py b/surfaces/cli/wizard/configurators/chat_notifications.py index c729bc6536..3396af95a5 100644 --- a/surfaces/cli/wizard/configurators/chat_notifications.py +++ b/surfaces/cli/wizard/configurators/chat_notifications.py @@ -3,8 +3,10 @@ from __future__ import annotations from config.env_file import sync_env_secret, sync_env_values +from integrations.setup_flow import apply_setup +from integrations.slack.setup import SLACK_SETUP_SPEC from integrations.store import upsert_integration -from platform.terminal.theme import ERROR, GLYPH_ERROR, SECONDARY, WARNING +from platform.terminal.theme import SECONDARY, WARNING from surfaces.cli.wizard._ui import ( Choice, _choose, @@ -18,7 +20,6 @@ validate_discord_bot, validate_rocketchat, validate_rocketchat_webhook, - validate_slack_webhook, validate_telegram_bot, ) @@ -36,20 +37,15 @@ def _configure_slack() -> tuple[str, str]: ) creds = dict(credentials) + pre_collected: dict[str, str] = {} if mode in {"webhook", "both"}: - while True: - webhook_url = _prompt_value( - "Slack webhook URL", - default=_string_value(creds.get("webhook_url")), - secret=True, - ) - with _console.status("Validating Slack webhook...", spinner="dots"): - result = validate_slack_webhook(webhook_url=webhook_url) - _render_integration_result("Slack webhook", result) - if result.ok: - creds["webhook_url"] = webhook_url - break - _console.print(f"[{SECONDARY}]Try again or press Ctrl+C to cancel.[/]") + webhook_url = _prompt_value( + "Slack webhook URL", + default=_string_value(creds.get("webhook_url")), + secret=True, + ) + if webhook_url: + pre_collected["webhook_url"] = webhook_url if mode in {"socket", "both"}: bot_token = _prompt_value( @@ -62,19 +58,13 @@ def _configure_slack() -> tuple[str, str]: default=_string_value(creds.get("app_token")), secret=True, ) - if not bot_token.startswith("xoxb-") or not app_token.startswith("xapp-"): - _console.print( - f"[{ERROR}]{GLYPH_ERROR} Socket Mode needs xoxb- bot token and xapp- app token.[/]" - ) - raise SystemExit(1) - creds["bot_token"] = bot_token - creds["app_token"] = app_token - sync_env_secret("SLACK_BOT_TOKEN", bot_token) - sync_env_secret("SLACK_APP_TOKEN", app_token) + if bot_token: + pre_collected["bot_token"] = bot_token + if app_token: + pre_collected["app_token"] = app_token - upsert_integration("slack", {"credentials": creds}) - env_path = sync_env_values({}) - return "Slack", str(env_path) + apply_setup(SLACK_SETUP_SPEC, pre_collected) + return "Slack", "" def _configure_discord() -> tuple[str, str]: