From a3474dda33531b9dff46f09f65e95746efbcfc4c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Mar 2026 03:32:46 +0000 Subject: [PATCH] refactor(discord): share setup wizard base --- extensions/discord/src/setup-core.ts | 144 ++++++++------ extensions/discord/src/setup-surface.ts | 250 ++++++------------------ 2 files changed, 141 insertions(+), 253 deletions(-) diff --git a/extensions/discord/src/setup-core.ts b/extensions/discord/src/setup-core.ts index f8fd6986439..efcdac05c27 100644 --- a/extensions/discord/src/setup-core.ts +++ b/extensions/discord/src/setup-core.ts @@ -138,9 +138,43 @@ export const discordSetupAdapter: ChannelSetupAdapter = { }, }; -export function createDiscordSetupWizardProxy( - loadWizard: () => Promise<{ discordSetupWizard: ChannelSetupWizard }>, -) { +type DiscordAllowFromResolverParams = { + cfg: OpenClawConfig; + accountId: string; + credentialValues: { token?: string }; + entries: string[]; +}; + +type DiscordGroupAllowlistResolverParams = DiscordAllowFromResolverParams & { + prompter: { note: (message: string, title?: string) => Promise }; +}; + +type DiscordGroupAllowlistResolution = Array<{ + input: string; + resolved: boolean; +}>; + +type DiscordSetupWizardHandlers = { + promptAllowFrom: (params: { + cfg: OpenClawConfig; + prompter: import("../../../src/plugin-sdk-internal/setup.js").WizardPrompter; + accountId?: string; + }) => Promise; + resolveAllowFromEntries: (params: DiscordAllowFromResolverParams) => Promise< + Array<{ + input: string; + resolved: boolean; + id: string | null; + }> + >; + resolveGroupAllowlist: ( + params: DiscordGroupAllowlistResolverParams, + ) => Promise; +}; + +export function createDiscordSetupWizardBase( + handlers: DiscordSetupWizardHandlers, +): ChannelSetupWizard { const discordDmPolicy: ChannelSetupDmPolicy = { label: "Discord", channel, @@ -154,13 +188,7 @@ export function createDiscordSetupWizardProxy( channel, dmPolicy: policy, }), - promptAllowFrom: async ({ cfg, prompter, accountId }) => { - const wizard = (await loadWizard()).discordSetupWizard; - if (!wizard.dmPolicy?.promptAllowFrom) { - return cfg; - } - return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId }); - }, + promptAllowFrom: handlers.promptAllowFrom, }; return { @@ -238,44 +266,22 @@ export function createDiscordSetupWizardProxy( accountId, patch: { groupPolicy: policy }, }), - resolveAllowlist: async ({ - cfg, - accountId, - credentialValues, - entries, - prompter, - }: { - cfg: OpenClawConfig; - accountId: string; - credentialValues: { token?: string }; - entries: string[]; - prompter: { note: (message: string, title?: string) => Promise }; - }) => { - const wizard = (await loadWizard()).discordSetupWizard; - if (!wizard.groupAccess?.resolveAllowlist) { - return entries.map((input) => ({ input, resolved: false })); - } + resolveAllowlist: async (params: DiscordGroupAllowlistResolverParams) => { try { - return await wizard.groupAccess.resolveAllowlist({ - cfg, - accountId, - credentialValues, - entries, - prompter, - }); + return await handlers.resolveGroupAllowlist(params); } catch (error) { await noteChannelLookupFailure({ - prompter, + prompter: params.prompter, label: "Discord channels", error, }); await noteChannelLookupSummary({ - prompter, + prompter: params.prompter, label: "Discord channels", resolvedSections: [], - unresolved: entries, + unresolved: params.entries, }); - return entries.map((input) => ({ input, resolved: false })); + return params.entries.map((input) => ({ input, resolved: false })); } }, applyAllowlist: ({ @@ -305,28 +311,7 @@ export function createDiscordSetupWizardProxy( invalidWithoutCredentialNote: "Bot token missing; use numeric user ids (or mention form) only.", parseId: parseDiscordAllowFromId, - resolveEntries: async ({ - cfg, - accountId, - credentialValues, - entries, - }: { - cfg: OpenClawConfig; - accountId: string; - credentialValues: { token?: string }; - entries: string[]; - }) => { - const wizard = (await loadWizard()).discordSetupWizard; - 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: async ({ cfg, accountId, @@ -347,3 +332,42 @@ export function createDiscordSetupWizardProxy( disable: (cfg: OpenClawConfig) => setSetupChannelEnabled(cfg, channel, false), } satisfies ChannelSetupWizard; } + +export function createDiscordSetupWizardProxy( + loadWizard: () => Promise<{ discordSetupWizard: ChannelSetupWizard }>, +) { + return createDiscordSetupWizardBase({ + promptAllowFrom: async ({ cfg, prompter, accountId }) => { + const wizard = (await loadWizard()).discordSetupWizard; + if (!wizard.dmPolicy?.promptAllowFrom) { + return cfg; + } + return await wizard.dmPolicy.promptAllowFrom({ cfg, prompter, accountId }); + }, + resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) => { + const wizard = (await loadWizard()).discordSetupWizard; + 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()).discordSetupWizard; + if (!wizard.groupAccess?.resolveAllowlist) { + return entries.map((input) => ({ input, resolved: false })); + } + return (await wizard.groupAccess.resolveAllowlist({ + cfg, + accountId, + credentialValues, + entries, + prompter, + })) as DiscordGroupAllowlistResolution; + }, + }); +} diff --git a/extensions/discord/src/setup-surface.ts b/extensions/discord/src/setup-surface.ts index f1d91cf47a8..5f785db6f01 100644 --- a/extensions/discord/src/setup-surface.ts +++ b/extensions/discord/src/setup-surface.ts @@ -1,27 +1,14 @@ import { - DEFAULT_ACCOUNT_ID, noteChannelLookupFailure, noteChannelLookupSummary, type OpenClawConfig, - parseMentionOrPrefixedId, - patchChannelConfigForAccount, promptLegacyChannelAllowFrom, resolveSetupAccountId, - setLegacyChannelDmPolicyWithAllowFrom, - setSetupChannelEnabled, type WizardPrompter, } from "../../../src/plugin-sdk-internal/setup.js"; -import { - type ChannelSetupDmPolicy, - type ChannelSetupWizard, -} from "../../../src/plugin-sdk-internal/setup.js"; +import { type ChannelSetupWizard } from "../../../src/plugin-sdk-internal/setup.js"; import { formatDocsLink } from "../../../src/terminal/links.js"; -import { inspectDiscordAccount } from "./account-inspect.js"; -import { - listDiscordAccountIds, - resolveDefaultDiscordAccountId, - resolveDiscordAccount, -} from "./accounts.js"; +import { resolveDefaultDiscordAccountId, resolveDiscordAccount } from "./accounts.js"; import { normalizeDiscordSlug } from "./monitor/allow-list.js"; import { resolveDiscordChannelAllowlist, @@ -29,6 +16,7 @@ import { } from "./resolve-channels.js"; import { resolveDiscordUserAllowlist } from "./resolve-users.js"; import { + createDiscordSetupWizardBase, discordSetupAdapter, DISCORD_TOKEN_HELP_LINES, parseDiscordAllowFromId, @@ -94,186 +82,62 @@ async function promptDiscordAllowFrom(params: { }); } -const discordDmPolicy: ChannelSetupDmPolicy = { - label: "Discord", - channel, - policyKey: "channels.discord.dmPolicy", - allowFromKey: "channels.discord.allowFrom", - getCurrent: (cfg) => - cfg.channels?.discord?.dmPolicy ?? cfg.channels?.discord?.dm?.policy ?? "pairing", - setPolicy: (cfg, policy) => - setLegacyChannelDmPolicyWithAllowFrom({ - cfg, - channel, - dmPolicy: policy, - }), +export const discordSetupWizard: ChannelSetupWizard = createDiscordSetupWizardBase({ promptAllowFrom: promptDiscordAllowFrom, -}; - -export const discordSetupWizard: ChannelSetupWizard = { - channel, - status: { - configuredLabel: "configured", - unconfiguredLabel: "needs token", - configuredHint: "configured", - unconfiguredHint: "needs token", - configuredScore: 2, - unconfiguredScore: 1, - resolveConfigured: ({ cfg }) => - listDiscordAccountIds(cfg).some( - (accountId) => inspectDiscordAccount({ cfg, accountId }).configured, - ), - }, - credentials: [ - { - inputKey: "token", - providerHint: channel, - credentialLabel: "Discord bot token", - preferredEnvVar: "DISCORD_BOT_TOKEN", - helpTitle: "Discord bot token", - helpLines: DISCORD_TOKEN_HELP_LINES, - envPrompt: "DISCORD_BOT_TOKEN detected. Use env var?", - keepPrompt: "Discord token already configured. Keep it?", - inputPrompt: "Enter Discord bot token", - allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID, - inspect: ({ cfg, accountId }) => { - const account = inspectDiscordAccount({ cfg, accountId }); - return { - accountConfigured: account.configured, - hasConfiguredValue: account.tokenStatus !== "missing", - resolvedValue: account.token?.trim() || undefined, - envValue: - accountId === DEFAULT_ACCOUNT_ID - ? process.env.DISCORD_BOT_TOKEN?.trim() || undefined - : undefined, - }; - }, - }, - ], - groupAccess: { - label: "Discord channels", - placeholder: "My Server/#general, guildId/channelId, #support", - currentPolicy: ({ cfg, accountId }) => - resolveDiscordAccount({ cfg, accountId }).config.groupPolicy ?? "allowlist", - currentEntries: ({ cfg, accountId }) => - Object.entries(resolveDiscordAccount({ cfg, accountId }).config.guilds ?? {}).flatMap( - ([guildKey, value]) => { - const channels = value?.channels ?? {}; - const channelKeys = Object.keys(channels); - if (channelKeys.length === 0) { - const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey; - return [input]; - } - return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`); - }, - ), - updatePrompt: ({ cfg, accountId }) => - Boolean(resolveDiscordAccount({ cfg, accountId }).config.guilds), - setPolicy: ({ cfg, accountId, policy }) => - patchChannelConfigForAccount({ - cfg, - channel, - accountId, - patch: { groupPolicy: policy }, - }), - resolveAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => { - const token = + resolveAllowFromEntries: async ({ cfg, accountId, credentialValues, entries }) => + await resolveDiscordAllowFromEntries({ + token: resolveDiscordAccount({ cfg, accountId }).token || - (typeof credentialValues.token === "string" ? credentialValues.token : ""); - let resolved: DiscordChannelResolution[] = entries.map((input) => ({ - input, - resolved: false, - })); - if (!token || entries.length === 0) { - return resolved; - } - try { - resolved = await resolveDiscordChannelAllowlist({ - token, - entries, - }); - const resolvedChannels = resolved.filter((entry) => entry.resolved && entry.channelId); - const resolvedGuilds = resolved.filter( - (entry) => entry.resolved && entry.guildId && !entry.channelId, - ); - const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input); - await noteChannelLookupSummary({ - prompter, - label: "Discord channels", - resolvedSections: [ - { - title: "Resolved channels", - values: resolvedChannels - .map((entry) => entry.channelId) - .filter((value): value is string => Boolean(value)), - }, - { - title: "Resolved guilds", - values: resolvedGuilds - .map((entry) => entry.guildId) - .filter((value): value is string => Boolean(value)), - }, - ], - unresolved, - }); - } catch (error) { - await noteChannelLookupFailure({ - prompter, - label: "Discord channels", - error, - }); - } + (typeof credentialValues.token === "string" ? credentialValues.token : ""), + entries, + }), + resolveGroupAllowlist: async ({ cfg, accountId, credentialValues, entries, prompter }) => { + const token = + resolveDiscordAccount({ cfg, accountId }).token || + (typeof credentialValues.token === "string" ? credentialValues.token : ""); + let resolved: DiscordChannelResolution[] = entries.map((input) => ({ + input, + resolved: false, + })); + if (!token || entries.length === 0) { return resolved; - }, - applyAllowlist: ({ cfg, accountId, resolved }) => { - const allowlistEntries: Array<{ guildKey: string; channelKey?: string }> = []; - for (const entry of resolved as DiscordChannelResolution[]) { - const guildKey = - entry.guildId ?? - (entry.guildName ? normalizeDiscordSlug(entry.guildName) : undefined) ?? - "*"; - const channelKey = - entry.channelId ?? - (entry.channelName ? normalizeDiscordSlug(entry.channelName) : undefined); - if (!channelKey && guildKey === "*") { - continue; - } - allowlistEntries.push({ guildKey, ...(channelKey ? { channelKey } : {}) }); - } - return setDiscordGuildChannelAllowlist(cfg, accountId, allowlistEntries); - }, - }, - allowFrom: { - credentialInputKey: "token", - helpTitle: "Discord allowlist", - helpLines: [ - "Allowlist Discord DMs by username (we resolve to user ids).", - "Examples:", - "- 123456789012345678", - "- @alice", - "- alice#1234", - "Multiple entries: comma-separated.", - `Docs: ${formatDocsLink("/discord", "discord")}`, - ], - message: "Discord allowFrom (usernames or ids)", - placeholder: "@alice, 123456789012345678", - invalidWithoutCredentialNote: "Bot token missing; use numeric user ids (or mention form) only.", - parseId: parseDiscordAllowFromId, - resolveEntries: async ({ cfg, accountId, credentialValues, entries }) => - await resolveDiscordAllowFromEntries({ - token: - resolveDiscordAccount({ cfg, accountId }).token || - (typeof credentialValues.token === "string" ? credentialValues.token : ""), + } + try { + resolved = await resolveDiscordChannelAllowlist({ + token, entries, - }), - apply: async ({ cfg, accountId, allowFrom }) => - patchChannelConfigForAccount({ - cfg, - channel, - accountId, - patch: { dmPolicy: "allowlist", allowFrom }, - }), + }); + const resolvedChannels = resolved.filter((entry) => entry.resolved && entry.channelId); + const resolvedGuilds = resolved.filter( + (entry) => entry.resolved && entry.guildId && !entry.channelId, + ); + const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input); + await noteChannelLookupSummary({ + prompter, + label: "Discord channels", + resolvedSections: [ + { + title: "Resolved channels", + values: resolvedChannels + .map((entry) => entry.channelId) + .filter((value): value is string => Boolean(value)), + }, + { + title: "Resolved guilds", + values: resolvedGuilds + .map((entry) => entry.guildId) + .filter((value): value is string => Boolean(value)), + }, + ], + unresolved, + }); + } catch (error) { + await noteChannelLookupFailure({ + prompter, + label: "Discord channels", + error, + }); + } + return resolved; }, - dmPolicy: discordDmPolicy, - disable: (cfg) => setSetupChannelEnabled(cfg, channel, false), -}; +});