Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions integrations/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -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(
Expand All @@ -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 <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)")
Expand Down
59 changes: 59 additions & 0 deletions integrations/slack/setup.py
Original file line number Diff line number Diff line change
@@ -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,
)
5 changes: 5 additions & 0 deletions integrations/slack/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 17 additions & 27 deletions surfaces/cli/wizard/configurators/chat_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,7 +20,6 @@
validate_discord_bot,
validate_rocketchat,
validate_rocketchat_webhook,
validate_slack_webhook,
validate_telegram_bot,
)

Expand All @@ -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(
Expand All @@ -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]:
Expand Down