refactor(channels): share slack matching and allowlist prompt flow

This commit is contained in:
Peter Steinberger
2026-02-18 16:31:21 +00:00
parent c0cd53e104
commit f3b75730de
4 changed files with 133 additions and 139 deletions

View File

@@ -129,6 +129,41 @@ function resolveDiscordChannelEntry<TEntry>(
);
}
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<string, SlackChannelPolicyEntry>;
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,

View File

@@ -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 = {

View File

@@ -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<string | number>;
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<AllowFromResolution[]>;
}): Promise<string[]> {
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);
}
}

View File

@@ -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 = {