diff --git a/extensions/slack/src/setup-surface.ts b/extensions/slack/src/setup-surface.ts index 4e3670ac843..063129267cf 100644 --- a/extensions/slack/src/setup-surface.ts +++ b/extensions/slack/src/setup-surface.ts @@ -1,50 +1,22 @@ import { - DEFAULT_ACCOUNT_ID, - hasConfiguredSecretInput, noteChannelLookupFailure, noteChannelLookupSummary, - normalizeAccountId, type OpenClawConfig, parseMentionOrPrefixedId, - patchChannelConfigForAccount, promptLegacyChannelAllowFrom, resolveSetupAccountId, - setAccountGroupPolicyForChannel, - setLegacyChannelDmPolicyWithAllowFrom, - setSetupChannelEnabled, type WizardPrompter, } from "openclaw/plugin-sdk/setup"; import type { - ChannelSetupDmPolicy, ChannelSetupWizard, ChannelSetupWizardAllowFromEntry, } from "openclaw/plugin-sdk/setup"; import { formatDocsLink } from "../../../src/terminal/links.js"; -import { inspectSlackAccount } from "./account-inspect.js"; -import { - listSlackAccountIds, - resolveDefaultSlackAccountId, - resolveSlackAccount, - type ResolvedSlackAccount, -} from "./accounts.js"; +import { resolveDefaultSlackAccountId, resolveSlackAccount } from "./accounts.js"; import { resolveSlackChannelAllowlist } from "./resolve-channels.js"; import { resolveSlackUserAllowlist } from "./resolve-users.js"; -import { slackSetupAdapter } from "./setup-core.js"; -import { - buildSlackSetupLines, - isSlackSetupAccountConfigured, - setSlackChannelAllowlist, - SLACK_CHANNEL as channel, -} from "./shared.js"; - -function enableSlackAccount(cfg: OpenClawConfig, accountId: string): OpenClawConfig { - return patchChannelConfigForAccount({ - cfg, - channel, - accountId, - patch: { enabled: true }, - }); -} +import { createSlackSetupWizardBase } from "./setup-core.js"; +import { SLACK_CHANNEL as channel } from "./shared.js"; async function resolveSlackAllowFromEntries(params: { token?: string; @@ -117,211 +89,68 @@ async function promptSlackAllowFrom(params: { }); } -const slackDmPolicy: ChannelSetupDmPolicy = { - label: "Slack", - channel, - policyKey: "channels.slack.dmPolicy", - allowFromKey: "channels.slack.allowFrom", - getCurrent: (cfg) => - cfg.channels?.slack?.dmPolicy ?? cfg.channels?.slack?.dm?.policy ?? "pairing", - setPolicy: (cfg, policy) => - setLegacyChannelDmPolicyWithAllowFrom({ - cfg, - channel, - dmPolicy: policy, - }), - promptAllowFrom: promptSlackAllowFrom, -}; - -export const slackSetupWizard: ChannelSetupWizard = { - channel, - status: { - configuredLabel: "configured", - unconfiguredLabel: "needs tokens", - configuredHint: "configured", - unconfiguredHint: "needs tokens", - configuredScore: 2, - unconfiguredScore: 1, - resolveConfigured: ({ cfg }) => - listSlackAccountIds(cfg).some((accountId) => { - const account = inspectSlackAccount({ cfg, accountId }); - return account.configured; - }), - }, - introNote: { - title: "Slack socket mode tokens", - lines: buildSlackSetupLines(), - shouldShow: ({ cfg, accountId }) => - !isSlackSetupAccountConfigured(resolveSlackAccount({ cfg, accountId })), - }, - envShortcut: { - prompt: "SLACK_BOT_TOKEN + SLACK_APP_TOKEN detected. Use env vars?", - preferredEnvVar: "SLACK_BOT_TOKEN", - isAvailable: ({ cfg, accountId }) => - accountId === DEFAULT_ACCOUNT_ID && - Boolean(process.env.SLACK_BOT_TOKEN?.trim()) && - Boolean(process.env.SLACK_APP_TOKEN?.trim()) && - !isSlackSetupAccountConfigured(resolveSlackAccount({ cfg, accountId })), - apply: ({ cfg, accountId }) => enableSlackAccount(cfg, accountId), - }, - credentials: [ - { - inputKey: "botToken", - providerHint: "slack-bot", - credentialLabel: "Slack bot token", - preferredEnvVar: "SLACK_BOT_TOKEN", - envPrompt: "SLACK_BOT_TOKEN detected. Use env var?", - keepPrompt: "Slack bot token already configured. Keep it?", - inputPrompt: "Enter Slack bot token (xoxb-...)", - allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID, - inspect: ({ cfg, accountId }) => { - const resolved = resolveSlackAccount({ cfg, accountId }); - return { - accountConfigured: - Boolean(resolved.botToken) || hasConfiguredSecretInput(resolved.config.botToken), - hasConfiguredValue: hasConfiguredSecretInput(resolved.config.botToken), - resolvedValue: resolved.botToken?.trim() || undefined, - envValue: - accountId === DEFAULT_ACCOUNT_ID ? process.env.SLACK_BOT_TOKEN?.trim() : undefined, - }; - }, - applyUseEnv: ({ cfg, accountId }) => enableSlackAccount(cfg, accountId), - applySet: ({ cfg, accountId, value }) => - patchChannelConfigForAccount({ - cfg, - channel, - accountId, - patch: { - enabled: true, - botToken: value, - }, - }), - }, - { - inputKey: "appToken", - providerHint: "slack-app", - credentialLabel: "Slack app token", - preferredEnvVar: "SLACK_APP_TOKEN", - envPrompt: "SLACK_APP_TOKEN detected. Use env var?", - keepPrompt: "Slack app token already configured. Keep it?", - inputPrompt: "Enter Slack app token (xapp-...)", - allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID, - inspect: ({ cfg, accountId }) => { - const resolved = resolveSlackAccount({ cfg, accountId }); - return { - accountConfigured: - Boolean(resolved.appToken) || hasConfiguredSecretInput(resolved.config.appToken), - hasConfiguredValue: hasConfiguredSecretInput(resolved.config.appToken), - resolvedValue: resolved.appToken?.trim() || undefined, - envValue: - accountId === DEFAULT_ACCOUNT_ID ? process.env.SLACK_APP_TOKEN?.trim() : undefined, - }; - }, - applyUseEnv: ({ cfg, accountId }) => enableSlackAccount(cfg, accountId), - applySet: ({ cfg, accountId, value }) => - patchChannelConfigForAccount({ - cfg, - channel, - accountId, - patch: { - enabled: true, - appToken: value, - }, - }), - }, - ], - dmPolicy: slackDmPolicy, - allowFrom: { - helpTitle: "Slack allowlist", - helpLines: [ - "Allowlist Slack DMs by username (we resolve to user ids).", - "Examples:", - "- U12345678", - "- @alice", - "Multiple entries: comma-separated.", - `Docs: ${formatDocsLink("/slack", "slack")}`, - ], - credentialInputKey: "botToken", - message: "Slack allowFrom (usernames or ids)", - placeholder: "@alice, U12345678", - invalidWithoutCredentialNote: "Slack token missing; use user ids (or mention form) only.", - parseId: (value) => - parseMentionOrPrefixedId({ - value, - mentionPattern: /^<@([A-Z0-9]+)>$/i, - prefixPattern: /^(slack:|user:)/i, - idPattern: /^[A-Z][A-Z0-9]+$/i, - normalizeId: (id) => id.toUpperCase(), - }), - resolveEntries: async ({ credentialValues, entries }) => - await resolveSlackAllowFromEntries({ - token: credentialValues.botToken, - entries, - }), - apply: ({ cfg, accountId, allowFrom }) => - patchChannelConfigForAccount({ - cfg, - channel, - accountId, - patch: { dmPolicy: "allowlist", allowFrom }, - }), - }, - groupAccess: { - label: "Slack channels", - placeholder: "#general, #private, C123", - currentPolicy: ({ cfg, accountId }) => - resolveSlackAccount({ cfg, accountId }).config.groupPolicy ?? "allowlist", - currentEntries: ({ cfg, accountId }) => - Object.entries(resolveSlackAccount({ cfg, accountId }).config.channels ?? {}) - .filter(([, value]) => value?.allow !== false && value?.enabled !== false) - .map(([key]) => key), - updatePrompt: ({ cfg, accountId }) => - Boolean(resolveSlackAccount({ cfg, accountId }).config.channels), - setPolicy: ({ cfg, accountId, policy }) => - setAccountGroupPolicyForChannel({ - cfg, - channel, - accountId, - groupPolicy: policy, - }), - resolveAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => { - let keys = entries; - const accountWithTokens = resolveSlackAccount({ - cfg, - accountId, +async function resolveSlackGroupAllowlist(params: { + cfg: OpenClawConfig; + accountId: string; + credentialValues: { botToken?: string }; + entries: string[]; + prompter: { note: (message: string, title?: string) => Promise }; +}) { + let keys = params.entries; + const accountWithTokens = resolveSlackAccount({ + cfg: params.cfg, + accountId: params.accountId, + }); + const activeBotToken = accountWithTokens.botToken || params.credentialValues.botToken || ""; + if (activeBotToken && params.entries.length > 0) { + try { + const resolved = await resolveSlackChannelAllowlist({ + token: activeBotToken, + entries: params.entries, }); - const activeBotToken = accountWithTokens.botToken || credentialValues.botToken || ""; - if (activeBotToken && entries.length > 0) { - try { - const resolved = await resolveSlackChannelAllowlist({ - token: activeBotToken, - entries, - }); - const resolvedKeys = resolved - .filter((entry) => entry.resolved && entry.id) - .map((entry) => entry.id as string); - const unresolved = resolved - .filter((entry) => !entry.resolved) - .map((entry) => entry.input); - keys = [...resolvedKeys, ...unresolved.map((entry) => entry.trim()).filter(Boolean)]; - await noteChannelLookupSummary({ - prompter, - label: "Slack channels", - resolvedSections: [{ title: "Resolved", values: resolvedKeys }], - unresolved, - }); - } catch (error) { - await noteChannelLookupFailure({ - prompter, - label: "Slack channels", - error, - }); - } - } - return keys; + const resolvedKeys = resolved + .filter((entry) => entry.resolved && entry.id) + .map((entry) => entry.id as string); + const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input); + keys = [...resolvedKeys, ...unresolved.map((entry) => entry.trim()).filter(Boolean)]; + await noteChannelLookupSummary({ + prompter: params.prompter, + label: "Slack channels", + resolvedSections: [{ title: "Resolved", values: resolvedKeys }], + unresolved, + }); + } catch (error) { + await noteChannelLookupFailure({ + prompter: params.prompter, + label: "Slack channels", + error, + }); + } + } + return keys; +} + +export const slackSetupWizard: ChannelSetupWizard = createSlackSetupWizardBase(async () => ({ + slackSetupWizard: { + dmPolicy: { + promptAllowFrom: promptSlackAllowFrom, }, - applyAllowlist: ({ cfg, accountId, resolved }) => - setSlackChannelAllowlist(cfg, accountId, resolved as string[]), - }, - disable: (cfg) => setSetupChannelEnabled(cfg, channel, false), -}; + allowFrom: { + resolveEntries: async ({ credentialValues, entries }) => + await resolveSlackAllowFromEntries({ + token: credentialValues.botToken, + entries, + }), + }, + groupAccess: { + resolveAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => + await resolveSlackGroupAllowlist({ + cfg, + accountId, + credentialValues, + entries, + prompter, + }), + }, + } as ChannelSetupWizard, +}));