refactor(discord): share setup wizard base

This commit is contained in:
Peter Steinberger 2026-03-17 05:22:43 +00:00
parent a6bee25247
commit f1df31eeef
1 changed files with 44 additions and 193 deletions

View File

@ -1,24 +1,12 @@
import {
DEFAULT_ACCOUNT_ID,
noteChannelLookupFailure,
noteChannelLookupSummary,
type OpenClawConfig,
parseMentionOrPrefixedId,
patchChannelConfigForAccount,
promptLegacyChannelAllowFrom,
resolveSetupAccountId,
setLegacyChannelDmPolicyWithAllowFrom,
setSetupChannelEnabled,
type WizardPrompter,
} from "openclaw/plugin-sdk/setup";
import { type ChannelSetupDmPolicy, type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
import { type ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
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,
@ -26,7 +14,7 @@ import {
} from "./resolve-channels.js";
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
import {
discordSetupAdapter,
createDiscordSetupWizardBase,
DISCORD_TOKEN_HELP_LINES,
parseDiscordAllowFromId,
setDiscordGuildChannelAllowlist,
@ -91,186 +79,49 @@ 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,
}),
promptAllowFrom: promptDiscordAllowFrom,
};
async function resolveDiscordGroupAllowlist(params: {
cfg: OpenClawConfig;
accountId: string;
credentialValues: { token?: string };
entries: string[];
}) {
const token =
resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }).token ||
(typeof params.credentialValues.token === "string" ? params.credentialValues.token : "");
if (!token || params.entries.length === 0) {
return params.entries.map((input) => ({
input,
resolved: false,
}));
}
return await resolveDiscordChannelAllowlist({
token,
entries: params.entries,
});
}
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,
};
},
export const discordSetupWizard: ChannelSetupWizard = createDiscordSetupWizardBase(async () => ({
discordSetupWizard: {
dmPolicy: {
promptAllowFrom: promptDiscordAllowFrom,
},
],
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 =
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,
groupAccess: {
resolveAllowlist: async ({ cfg, accountId, credentialValues, entries }) =>
await resolveDiscordGroupAllowlist({
cfg,
accountId,
credentialValues,
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,
});
}
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: {
resolveEntries: async ({ cfg, accountId, credentialValues, entries }) =>
await resolveDiscordAllowFromEntries({
token:
resolveDiscordAccount({ cfg, accountId }).token ||
(typeof credentialValues.token === "string" ? credentialValues.token : ""),
entries,
}),
},
},
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 : ""),
entries,
}),
apply: async ({ cfg, accountId, allowFrom }) =>
patchChannelConfigForAccount({
cfg,
channel,
accountId,
patch: { dmPolicy: "allowlist", allowFrom },
}),
},
dmPolicy: discordDmPolicy,
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
};
} as ChannelSetupWizard,
}));