diff --git a/src/channels/plugins/group-mentions.ts b/src/channels/plugins/group-mentions.ts index f52a58c0a..994f457ce 100644 --- a/src/channels/plugins/group-mentions.ts +++ b/src/channels/plugins/group-mentions.ts @@ -129,6 +129,41 @@ function resolveDiscordChannelEntry( ); } +type SlackChannelPolicyEntry = { + requireMention?: boolean; + tools?: GroupToolPolicyConfig; + toolsBySender?: GroupToolPolicyBySenderConfig; +}; + +function resolveSlackChannelPolicyEntry( + params: GroupMentionParams, +): SlackChannelPolicyEntry | undefined { + const account = resolveSlackAccount({ + cfg: params.cfg, + accountId: params.accountId, + }); + const channels = (account.channels ?? {}) as Record; + if (Object.keys(channels).length === 0) { + return undefined; + } + const channelId = params.groupId?.trim(); + const groupChannel = params.groupChannel; + const channelName = groupChannel?.replace(/^#/, ""); + const normalizedName = normalizeHyphenSlug(channelName); + const candidates = [ + channelId ?? "", + channelName ? `#${channelName}` : "", + channelName ?? "", + normalizedName, + ].filter(Boolean); + for (const candidate of candidates) { + if (candidate && channels[candidate]) { + return channels[candidate]; + } + } + return channels["*"]; +} + export function resolveTelegramGroupRequireMention( params: GroupMentionParams, ): boolean | undefined { @@ -210,34 +245,7 @@ export function resolveGoogleChatGroupToolPolicy( } export function resolveSlackGroupRequireMention(params: GroupMentionParams): boolean { - const account = resolveSlackAccount({ - cfg: params.cfg, - accountId: params.accountId, - }); - const channels = account.channels ?? {}; - const keys = Object.keys(channels); - if (keys.length === 0) { - return true; - } - const channelId = params.groupId?.trim(); - const groupChannel = params.groupChannel; - const channelName = groupChannel?.replace(/^#/, ""); - const normalizedName = normalizeHyphenSlug(channelName); - const candidates = [ - channelId ?? "", - channelName ? `#${channelName}` : "", - channelName ?? "", - normalizedName, - ].filter(Boolean); - let matched: { requireMention?: boolean } | undefined; - for (const candidate of candidates) { - if (candidate && channels[candidate]) { - matched = channels[candidate]; - break; - } - } - const fallback = channels["*"]; - const resolved = matched ?? fallback; + const resolved = resolveSlackChannelPolicyEntry(params); if (typeof resolved?.requireMention === "boolean") { return resolved.requireMention; } @@ -342,35 +350,10 @@ export function resolveDiscordGroupToolPolicy( export function resolveSlackGroupToolPolicy( params: GroupMentionParams, ): GroupToolPolicyConfig | undefined { - const account = resolveSlackAccount({ - cfg: params.cfg, - accountId: params.accountId, - }); - const channels = account.channels ?? {}; - const keys = Object.keys(channels); - if (keys.length === 0) { + const resolved = resolveSlackChannelPolicyEntry(params); + if (!resolved) { return undefined; } - const channelId = params.groupId?.trim(); - const groupChannel = params.groupChannel; - const channelName = groupChannel?.replace(/^#/, ""); - const normalizedName = normalizeHyphenSlug(channelName); - const candidates = [ - channelId ?? "", - channelName ? `#${channelName}` : "", - channelName ?? "", - normalizedName, - ].filter(Boolean); - let matched: - | { tools?: GroupToolPolicyConfig; toolsBySender?: GroupToolPolicyBySenderConfig } - | undefined; - for (const candidate of candidates) { - if (candidate && channels[candidate]) { - matched = channels[candidate]; - break; - } - } - const resolved = matched ?? channels["*"]; const senderPolicy = resolveToolsBySender({ toolsBySender: resolved?.toolsBySender, senderId: params.senderId, diff --git a/src/channels/plugins/onboarding/discord.ts b/src/channels/plugins/onboarding/discord.ts index 7b2dc844b..45410ee4e 100644 --- a/src/channels/plugins/onboarding/discord.ts +++ b/src/channels/plugins/onboarding/discord.ts @@ -17,7 +17,7 @@ import { formatDocsLink } from "../../../terminal/links.js"; import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; import { promptChannelAccessConfig } from "./channel-access.js"; -import { addWildcardAllowFrom, mergeAllowFromEntries, promptAccountId } from "./helpers.js"; +import { addWildcardAllowFrom, promptAccountId, promptResolvedAllowFrom } from "./helpers.js"; const channel = "discord" as const; @@ -195,47 +195,23 @@ async function promptDiscordAllowFrom(params: { return null; }; - while (true) { - const entry = await params.prompter.text({ - message: "Discord allowFrom (usernames or ids)", - placeholder: "@alice, 123456789012345678", - initialValue: existing[0] ? String(existing[0]) : undefined, - validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), - }); - const parts = parseInputs(String(entry)); - if (!token) { - const ids = parts.map(parseId).filter(Boolean) as string[]; - if (ids.length !== parts.length) { - await params.prompter.note( - "Bot token missing; use numeric user ids (or mention form) only.", - "Discord allowlist", - ); - continue; - } - const unique = mergeAllowFromEntries(existing, ids); - return setDiscordAllowFrom(params.cfg, unique); - } - - const results = await resolveDiscordUserAllowlist({ - token, - entries: parts, - }).catch(() => null); - if (!results) { - await params.prompter.note("Failed to resolve usernames. Try again.", "Discord allowlist"); - continue; - } - const unresolved = results.filter((res) => !res.resolved || !res.id); - if (unresolved.length > 0) { - await params.prompter.note( - `Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`, - "Discord allowlist", - ); - continue; - } - const ids = results.map((res) => res.id as string); - const unique = mergeAllowFromEntries(existing, ids); - return setDiscordAllowFrom(params.cfg, unique); - } + const unique = await promptResolvedAllowFrom({ + prompter: params.prompter, + existing, + token, + message: "Discord allowFrom (usernames or ids)", + placeholder: "@alice, 123456789012345678", + label: "Discord allowlist", + parseInputs, + parseId, + invalidWithoutTokenNote: "Bot token missing; use numeric user ids (or mention form) only.", + resolveEntries: ({ token, entries }) => + resolveDiscordUserAllowlist({ + token, + entries, + }), + }); + return setDiscordAllowFrom(params.cfg, unique); } const dmPolicy: ChannelOnboardingDmPolicy = { diff --git a/src/channels/plugins/onboarding/helpers.ts b/src/channels/plugins/onboarding/helpers.ts index 559e88363..f31f0768f 100644 --- a/src/channels/plugins/onboarding/helpers.ts +++ b/src/channels/plugins/onboarding/helpers.ts @@ -1,4 +1,5 @@ import { promptAccountId as promptAccountIdSdk } from "../../../plugin-sdk/onboarding.js"; +import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { PromptAccountId, PromptAccountIdParams } from "../onboarding-types.js"; export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => { @@ -20,3 +21,61 @@ export function mergeAllowFromEntries( const merged = [...(current ?? []), ...additions].map((v) => String(v).trim()).filter(Boolean); return [...new Set(merged)]; } + +type AllowFromResolution = { + input: string; + resolved: boolean; + id?: string | null; +}; + +export async function promptResolvedAllowFrom(params: { + prompter: WizardPrompter; + existing: Array; + token?: string | null; + message: string; + placeholder: string; + label: string; + parseInputs: (value: string) => string[]; + parseId: (value: string) => string | null; + invalidWithoutTokenNote: string; + resolveEntries: (params: { token: string; entries: string[] }) => Promise; +}): Promise { + while (true) { + const entry = await params.prompter.text({ + message: params.message, + placeholder: params.placeholder, + initialValue: params.existing[0] ? String(params.existing[0]) : undefined, + validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + }); + const parts = params.parseInputs(String(entry)); + if (!params.token) { + const ids = parts.map(params.parseId).filter(Boolean) as string[]; + if (ids.length !== parts.length) { + await params.prompter.note(params.invalidWithoutTokenNote, params.label); + continue; + } + return mergeAllowFromEntries(params.existing, ids); + } + + const results = await params + .resolveEntries({ + token: params.token, + entries: parts, + }) + .catch(() => null); + if (!results) { + await params.prompter.note("Failed to resolve usernames. Try again.", params.label); + continue; + } + const unresolved = results.filter((res) => !res.resolved || !res.id); + if (unresolved.length > 0) { + await params.prompter.note( + `Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`, + params.label, + ); + continue; + } + const ids = results.map((res) => res.id as string); + return mergeAllowFromEntries(params.existing, ids); + } +} diff --git a/src/channels/plugins/onboarding/slack.ts b/src/channels/plugins/onboarding/slack.ts index 136b73ef8..81cbdff76 100644 --- a/src/channels/plugins/onboarding/slack.ts +++ b/src/channels/plugins/onboarding/slack.ts @@ -12,7 +12,7 @@ import { formatDocsLink } from "../../../terminal/links.js"; import type { WizardPrompter } from "../../../wizard/prompts.js"; import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js"; import { promptChannelAccessConfig } from "./channel-access.js"; -import { addWildcardAllowFrom, mergeAllowFromEntries, promptAccountId } from "./helpers.js"; +import { addWildcardAllowFrom, promptAccountId, promptResolvedAllowFrom } from "./helpers.js"; const channel = "slack" as const; @@ -263,47 +263,23 @@ async function promptSlackAllowFrom(params: { return null; }; - while (true) { - const entry = await params.prompter.text({ - message: "Slack allowFrom (usernames or ids)", - placeholder: "@alice, U12345678", - initialValue: existing[0] ? String(existing[0]) : undefined, - validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), - }); - const parts = parseInputs(String(entry)); - if (!token) { - const ids = parts.map(parseId).filter(Boolean) as string[]; - if (ids.length !== parts.length) { - await params.prompter.note( - "Slack token missing; use user ids (or mention form) only.", - "Slack allowlist", - ); - continue; - } - const unique = mergeAllowFromEntries(existing, ids); - return setSlackAllowFrom(params.cfg, unique); - } - - const results = await resolveSlackUserAllowlist({ - token, - entries: parts, - }).catch(() => null); - if (!results) { - await params.prompter.note("Failed to resolve usernames. Try again.", "Slack allowlist"); - continue; - } - const unresolved = results.filter((res) => !res.resolved || !res.id); - if (unresolved.length > 0) { - await params.prompter.note( - `Could not resolve: ${unresolved.map((res) => res.input).join(", ")}`, - "Slack allowlist", - ); - continue; - } - const ids = results.map((res) => res.id as string); - const unique = mergeAllowFromEntries(existing, ids); - return setSlackAllowFrom(params.cfg, unique); - } + const unique = await promptResolvedAllowFrom({ + prompter: params.prompter, + existing, + token, + message: "Slack allowFrom (usernames or ids)", + placeholder: "@alice, U12345678", + label: "Slack allowlist", + parseInputs, + parseId, + invalidWithoutTokenNote: "Slack token missing; use user ids (or mention form) only.", + resolveEntries: ({ token, entries }) => + resolveSlackUserAllowlist({ + token, + entries, + }), + }); + return setSlackAllowFrom(params.cfg, unique); } const dmPolicy: ChannelOnboardingDmPolicy = {