diff --git a/extensions/slack/src/setup-core.ts b/extensions/slack/src/setup-core.ts index a0f068b3e81..80369d417a7 100644 --- a/extensions/slack/src/setup-core.ts +++ b/extensions/slack/src/setup-core.ts @@ -110,9 +110,30 @@ export const slackSetupAdapter: ChannelSetupAdapter = { }, }; -export function createSlackSetupWizardProxy( - loadWizard: () => Promise<{ slackSetupWizard: ChannelSetupWizard }>, -) { +type SlackAllowFromResolverParams = { + cfg: OpenClawConfig; + accountId: string; + credentialValues: { botToken?: string }; + entries: string[]; +}; + +type SlackGroupAllowlistResolverParams = SlackAllowFromResolverParams & { + prompter: { note: (message: string, title?: string) => Promise }; +}; + +type SlackSetupWizardHandlers = { + promptAllowFrom: (params: { + cfg: OpenClawConfig; + prompter: import("../../../src/plugin-sdk-internal/setup.js").WizardPrompter; + accountId?: string; + }) => Promise; + resolveAllowFromEntries: ( + params: SlackAllowFromResolverParams, + ) => Promise; + resolveGroupAllowlist: (params: SlackGroupAllowlistResolverParams) => Promise; +}; + +export function createSlackSetupWizardBase(handlers: SlackSetupWizardHandlers): ChannelSetupWizard { const slackDmPolicy: ChannelSetupDmPolicy = { label: "Slack", channel, @@ -126,13 +147,7 @@ export function createSlackSetupWizardProxy( channel, dmPolicy: policy, }), - promptAllowFrom: async ({ cfg, prompter, accountId }) => { - const wizard = (await loadWizard()).slackSetupWizard; - if (!wizard.dmPolicy?.promptAllowFrom) { - return cfg; - } - return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId }); - }, + promptAllowFrom: handlers.promptAllowFrom, }; return { @@ -273,28 +288,7 @@ export function createSlackSetupWizardProxy( idPattern: /^[A-Z][A-Z0-9]+$/i, normalizeId: (id) => id.toUpperCase(), }), - resolveEntries: async ({ - cfg, - accountId, - credentialValues, - entries, - }: { - cfg: OpenClawConfig; - accountId: string; - credentialValues: { botToken?: string }; - entries: string[]; - }) => { - const wizard = (await loadWizard()).slackSetupWizard; - if (!wizard.allowFrom) { - return entries.map((input) => ({ input, resolved: false, id: null })); - } - return await wizard.allowFrom.resolveEntries({ - cfg, - accountId, - credentialValues, - entries, - }); - }, + resolveEntries: handlers.resolveAllowFromEntries, apply: ({ cfg, accountId, @@ -337,44 +331,22 @@ export function createSlackSetupWizardProxy( accountId, groupPolicy: policy, }), - resolveAllowlist: async ({ - cfg, - accountId, - credentialValues, - entries, - prompter, - }: { - cfg: OpenClawConfig; - accountId: string; - credentialValues: { botToken?: string }; - entries: string[]; - prompter: { note: (message: string, title?: string) => Promise }; - }) => { + resolveAllowlist: async (params: SlackGroupAllowlistResolverParams) => { try { - const wizard = (await loadWizard()).slackSetupWizard; - if (!wizard.groupAccess?.resolveAllowlist) { - return entries; - } - return await wizard.groupAccess.resolveAllowlist({ - cfg, - accountId, - credentialValues, - entries, - prompter, - }); + return await handlers.resolveGroupAllowlist(params); } catch (error) { await noteChannelLookupFailure({ - prompter, + prompter: params.prompter, label: "Slack channels", error, }); await noteChannelLookupSummary({ - prompter, + prompter: params.prompter, label: "Slack channels", resolvedSections: [], - unresolved: entries, + unresolved: params.entries, }); - return entries; + return params.entries; } }, applyAllowlist: ({ @@ -390,3 +362,42 @@ export function createSlackSetupWizardProxy( disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false), } satisfies ChannelSetupWizard; } + +export function createSlackSetupWizardProxy( + loadWizard: () => Promise<{ slackSetupWizard: ChannelSetupWizard }>, +) { + return createSlackSetupWizardBase({ + promptAllowFrom: async ({ cfg, prompter, accountId }) => { + const wizard = (await loadWizard()).slackSetupWizard; + if (!wizard.dmPolicy?.promptAllowFrom) { + return cfg; + } + return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId }); + }, + resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) => { + const wizard = (await loadWizard()).slackSetupWizard; + if (!wizard.allowFrom) { + return entries.map((input) => ({ input, resolved: false, id: null })); + } + return await wizard.allowFrom.resolveEntries({ + cfg, + accountId, + credentialValues, + entries, + }); + }, + resolveGroupAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => { + const wizard = (await loadWizard()).slackSetupWizard; + if (!wizard.groupAccess?.resolveAllowlist) { + return entries; + } + return (await wizard.groupAccess.resolveAllowlist({ + cfg, + accountId, + credentialValues, + entries, + prompter, + })) as string[]; + }, + }); +} diff --git a/extensions/slack/src/setup-surface.ts b/extensions/slack/src/setup-surface.ts index de7dc06e40e..8f5024276ca 100644 --- a/extensions/slack/src/setup-surface.ts +++ b/extensions/slack/src/setup-surface.ts @@ -1,50 +1,22 @@ import { - DEFAULT_ACCOUNT_ID, formatDocsLink, - hasConfiguredSecretInput, noteChannelLookupFailure, noteChannelLookupSummary, - normalizeAccountId, type OpenClawConfig, parseMentionOrPrefixedId, - patchChannelConfigForAccount, promptLegacyChannelAllowFrom, resolveSetupAccountId, - setAccountGroupPolicyForChannel, - setLegacyChannelDmPolicyWithAllowFrom, - setSetupChannelEnabled, type WizardPrompter, } from "../../../src/plugin-sdk-internal/setup.js"; import type { - ChannelSetupDmPolicy, ChannelSetupWizard, ChannelSetupWizardAllowFromEntry, } from "../../../src/plugin-sdk-internal/setup.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,45 @@ 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, - }), +export const slackSetupWizard: ChannelSetupWizard = createSlackSetupWizardBase({ 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, - }); - 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, - }); - } + resolveAllowFromEntries: async ({ credentialValues, entries }) => + await resolveSlackAllowFromEntries({ + token: credentialValues.botToken, + entries, + }), + resolveGroupAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => { + let keys = entries; + const accountWithTokens = resolveSlackAccount({ + cfg, + accountId, + }); + 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; - }, - applyAllowlist: ({ cfg, accountId, resolved }) => - setSlackChannelAllowlist(cfg, accountId, resolved as string[]), + } + return keys; }, - disable: (cfg) => setSetupChannelEnabled(cfg, channel, false), -}; +});